# -*- encoding: utf-8 -*-
require 'yaml'
require 'strscan'
require 'webgen/tag'
module Webgen::ContentProcessor
# Processes special webgen tags to provide dynamic content.
#
# webgen tags are an easy way to add dynamically generated content to websites, for example menus
# or breadcrumb trails.
class Tags
include Webgen::WebsiteAccess
include Webgen::Loggable
def initialize #:nodoc:
@start_re = /(\\*)\{#{website.config['contentprocessor.tags.prefix']}(\w+)(::?)/
@end_re = /(\\*)\{#{website.config['contentprocessor.tags.prefix']}(\w+)\}/
end
# Replace all webgen tags in the content of +context+ with the rendered content.
def call(context)
replace_tags(context) do |tag, param_string, body|
log(:debug) { "Replacing tag #{tag} with data '#{param_string}' and body '#{body}' in <#{context.ref_node}>" }
process_tag(tag, param_string, body, context)
end
context
end
# Process the +tag+ and return the result. The parameter +params+ needs to be a Hash holding all
# needed and optional parameters for the tag or a parameter String in YAML format and +body+ is
# the optional body for the tag. +context+ needs to be a valid Webgen::Context object.
def process_tag(tag, params, body, context)
result = ''
processor = processor_for_tag(tag)
if !processor.nil?
params = if params.kind_of?(String)
processor.create_tag_params(params, context.ref_node)
else
processor.create_params_hash(params, context.ref_node)
end
processor.set_params(params)
result, process_output = processor.call(tag, body, context)
processor.set_params(nil)
result = call(context.clone(:content => result)).content if process_output
else
raise Webgen::RenderError.new("No tag processor for '#{tag}' found", self.class.name,
context.dest_node, context.ref_node)
end
result
end
#######
private
#######
BRACKETS_RE = /([{}])/
ProcessingStruct = Struct.new(:state, :tag, :simple_tag, :backslashes, :brackets, :start_pos, :end_pos,
:params_start_pos, :params_end_pos, :body_end_pos)
# Return the context.content provided by context.ref_node with all webgen tags
# replaced. When a webgen tag is encountered by the parser, the method yields all found
# information and substitutes the returned string for the tag.
def replace_tags(context) #:yields: tag_name, param_string, body
scanner = StringScanner.new(context.content)
data = ProcessingStruct.new(:before_tag)
while true
case data.state
when :before_tag
if scanner.skip_until(@start_re)
data.state = :in_start_tag
data.backslashes = scanner[1].length
data.brackets = 1
data.tag = scanner[2]
data.simple_tag = (scanner[3] == ':')
data.params_start_pos = scanner.pos
data.start_pos = scanner.pos - scanner.matched.length
else
data.state = :done
end
when :in_start_tag
data.brackets += (scanner[1] == '{' ? 1 : -1) while data.brackets != 0 && scanner.skip_until(BRACKETS_RE)
if data.brackets != 0
raise Webgen::RenderError.new("Unbalanced curly brackets for tag '#{data.tag}'", self.class.name,
context.dest_node, context.ref_node)
else
data.params_end_pos = data.body_end_pos = data.end_pos = scanner.pos - 1
data.state = (data.simple_tag ? :process : :in_body)
end
when :process
if RUBY_VERSION >= '1.9'
begin
enc = scanner.string.encoding
scanner.string.force_encoding('ASCII-8BIT')
if data.backslashes % 2 == 0
result = yield(data.tag, scanner.string[data.params_start_pos...data.params_end_pos].force_encoding(enc),
scanner.string[(data.params_end_pos+1)...data.body_end_pos].to_s.force_encoding(enc)).to_s
scanner.string[data.start_pos..data.end_pos] = "\\" * (data.backslashes / 2) + result.force_encoding('ASCII-8BIT')
scanner.pos = data.start_pos + result.length
else
scanner.string[data.start_pos, 1 + data.backslashes / 2] = ''
scanner.pos -= 1 + data.backslashes / 2
end
ensure
scanner.string.force_encoding(enc) if RUBY_VERSION >= '1.9'
end
else
if data.backslashes % 2 == 0
result = yield(data.tag, scanner.string[data.params_start_pos...data.params_end_pos],
scanner.string[(data.params_end_pos+1)...data.body_end_pos]).to_s
scanner.string[data.start_pos..data.end_pos] = "\\" * (data.backslashes / 2) + result
scanner.pos = data.start_pos + result.length
else
scanner.string[data.start_pos, 1 + data.backslashes / 2] = ''
scanner.pos -= 1 + data.backslashes / 2
end
end
data.state = :before_tag
when :in_body
while (result = scanner.skip_until(@end_re))
next unless scanner[2] == data.tag
if scanner[1].length % 2 == 1
scanner.string[(scanner.pos - scanner.matched.length), 1 + scanner[1].length / 2] = ''
scanner.pos -= 1 + scanner[1].length / 2
else
break
end
end
if result
data.state = :process
data.end_pos = scanner.pos - 1
data.body_end_pos = scanner.pos - scanner.matched.length + scanner[1].length / 2
else
raise Webgen::RenderError.new("Invalid body part - no end tag found for '#{data.tag}'", self.class.name,
context.dest_node, context.ref_node)
end
when :done
break
end
end
scanner.string
rescue Webgen::RenderError => e
e.line = scanner.string[0...scanner.pos].scan("\n").size + 1 unless e.line
raise
end
# Return the tag processor for +tag+ or +nil+ if +tag+ is unknown.
def processor_for_tag(tag)
map = website.config['contentprocessor.tags.map']
klass = if map.has_key?(tag)
map[tag]
elsif map.has_key?(:default)
map[:default]
else
nil
end
klass.nil? ? nil : website.cache.instance(klass)
end
end
end