[#604] Evaluate Liquid before Textile-to-HTML transformation.

This changes how the liquid integration works. It now integrates the Textile
conversion step. This was necessary because if you first convert the snippets
inside of loops and conditionals from Textile to HTML, you loose some
important context information which is required to e.g. build proper lists in
textile.

We expect the standard case that Liquid tags return Textile markup instead
of HTML. Thus, we can convert the final textile markup to HTML as a very last
step.

To allow existing and new macros (or tags) to return HTML for advanced usage,
we save their respective output into the context and put a placeholder string
into the generated markup. After the transformation to HTML, we insert the
previously generated HTML into the string using search+replace in
lib/chili_project/liquid/template.rb. Tags have to be registered using
:html => true for this special treatment.
This commit is contained in:
Holger Just 2011-09-09 23:35:24 +02:00
parent 72fa3ff920
commit 82432f3f99
6 changed files with 176 additions and 6 deletions

View File

@ -462,14 +462,14 @@ module ApplicationHelper
only_path = options.delete(:only_path) == false ? false : true
begin
text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
liquid_template = Liquid::Template.parse(text)
liquid_template = ChiliProject::Liquid::Template.parse(text)
liquid_variables = get_view_instance_variables_for_liquid
liquid_variables.merge!({'current_user' => User.current})
liquid_variables.merge!({'toc' => '{{toc}}'}) # Pass toc through to replace later
liquid_variables.merge!(ChiliProject::Liquid::Variables.macro_backwards_compatibility)
# Pass :view in a register so this view (with helpers) can be used inside of a tag
text = liquid_template.render(liquid_variables, :registers => {:view => self})
text = liquid_template.render(liquid_variables, :registers => {:view => self, :object => obj, :attribute => attr})
# Add Liquid errors to the log
if Rails.logger && Rails.logger.debug?
@ -480,7 +480,6 @@ module ApplicationHelper
end
Rails.logger.debug msg
end
text
rescue Liquid::SyntaxError
# Skip Liquid if there is a syntax error
end
@ -962,7 +961,7 @@ module ApplicationHelper
# Expands the current menu item using JavaScript based on the params
def expand_current_menu
current_menu_class =
case
case
when params[:controller] == "timelog"
"reports"
when params[:controller] == 'projects' && params[:action] == 'changelog'
@ -981,7 +980,7 @@ module ApplicationHelper
params[:controller]
end
javascript_tag("jQuery.menu_expand({ menuItem: '.#{current_menu_class}' });")
end

View File

@ -0,0 +1,7 @@
require 'chili_project/liquid/liquid_ext'
require 'chili_project/liquid/tags'
module ChiliProject
module Liquid
end
end

View File

@ -0,0 +1,7 @@
module ChiliProject
module Liquid
module LiquidExt
::Liquid::Context.send(:include, Context)
end
end
end

View File

@ -0,0 +1,29 @@
module ChiliProject
module Liquid
module LiquidExt
module Context
def self.included(base)
base.send(:include, InstanceMethods)
end
module InstanceMethods
def html_result(html)
key = nil
while key.nil? || html_results.has_key?(key)
random = ActiveSupport::SecureRandom.hex(10)
# This string must be passed untouched through Liquid and textile
# It mustn't be changed in any way by any rendering stage.
key = "!!html_results.#{random}!!"
end
html_results[key] = html
key
end
def html_results
registers[:html_results] ||= {}
end
end
end
end
end
end

View File

@ -0,0 +1,32 @@
module ChiliProject::Liquid
module Tags
class TagError < StandardError; end
def self.register_tag(name, klass, options={})
if options[:html]
html_class = Class.new do
def render(context)
result = @tag.render(context)
context.html_result(result)
end
def method_missing(*args, &block)
@tag.send(*args, &block)
end
end
html_class.send :define_method, :initialize do |*args|
@tag = klass.new(*args)
end
::Liquid::Template.register_tag(name, html_class)
else
::Liquid::Template.register_tag(name, klass)
end
end
# TODO: reimplement old macros as tags and register them here
# child_pages
# hello_world
# include
# macro_list
end
end

View File

@ -0,0 +1,96 @@
module ChiliProject
module Liquid
class Template < ::Liquid::Template
# creates a new <tt>Template</tt> object from liquid source code
def self.parse(source)
template = self.new
template.parse(source)
template
end
def context_from_render_options(*args)
# This method is pulled out straight from the original
# Liquid::Template#render
context = case args.first
when ::Liquid::Context
args.shift
when Hash
::Liquid::Context.new([args.shift, assigns], instance_assigns, registers, @rethrow_errors)
when nil
::Liquid::Context.new(assigns, instance_assigns, registers, @rethrow_errors)
else
raise ArgumentError, "Expect Hash or Liquid::Context as parameter"
end
case args.last
when Hash
options = args.pop
if options[:registers].is_a?(Hash)
self.registers.merge!(options[:registers])
end
if options[:filters]
context.add_filters(options[:filters])
end
when Module
context.add_filters(args.pop)
when Array
context.add_filters(args.pop)
end
context
end
# Render takes a hash with local variables.
#
# if you use the same filters over and over again consider registering them globally
# with <tt>Template.register_filter</tt>
#
# Following options can be passed:
#
# * <tt>filters</tt> : array with local filters
# * <tt>registers</tt> : hash with register variables. Those can be accessed from
# filters and tags and might be useful to integrate liquid more with its host application
#
def render(*args)
return '' if @root.nil?
context = context_from_render_options(*args)
context.registers[:html_results] ||= {}
# ENTER THE RENDERING STAGE
# 1. Render the input as Liquid
# Some tags might register final HTML output in this stage.
begin
# for performance reasons we get a array back here. join will make a string out of it
result = @root.render(context)
result.respond_to?(:join) ? result.join : result
ensure
@errors = context.errors
end
# 2. Perform the Wiki markup transformation (e.g. Textile)
obj = context.registers[:object]
attr = context.registers[:attribute]
result = Redmine::WikiFormatting.to_html(Setting.text_formatting, result, :object => obj, :attribute => attr)
# 3. Now finally, replace the captured raw HTML bits in the final content
length = nil
# replace HTML results until we can find no additional variables
while length != context.registers[:html_results].length do
length = context.registers[:html_results].length
context.registers[:html_results].delete_if do |key, value|
# We use the block variant to avoid the evaluation of escaped
# characters in +value+ during substitution.
result.sub!(key) { |match| value }
end
end
result
end
end
end
end