require 'amrita/node.rb'
require 'amrita/node_expand.rb'
require 'amrita/format.rb'
require 'amrita/compiler.rb'
require 'amrita/parser.rb'
module Amrita
module CacheManager
Item = Struct.new(:type, :filename, :key, :mtime, :contents)
def cache(filename, typ, source_mtime=nil, key=nil, &block)
source_mtime = Time.new unless source_mtime
item = get_item(typ, filename, key) || Item.new
unless valid_item?(item, source_mtime)
item.filename = filename
item.type = typ
item.key = key
item.mtime = source_mtime
item.contents = yield
save_item(item)
end
item.contents
end
def valid_item?(item, source_mtime)
item.mtime && source_mtime && item.mtime >= source_mtime
end
end
class DummyCacheManager
include CacheManager
def get_item(typ, filename, key)
nil
end
def save_item(item)
# do nothing because it's dummy
end
end
class Template
include Amrita
# output is pretty printed if set.
attr_accessor :prettyprint
# compact spaces and delete new line of output if set.
# you can't set prettyprint and compact_space both in same Template.
attr_accessor :compact_space
# keep id attribute of output if set.
attr_accessor :keep_id
# The name of attribute that turns into _id_.
# You will need to set this if you use _id_ attribute for DOM/CSS/etc...
# For expample, if this was set to "__id",
# you can use _id_ for amrita and __id for DOM/CSS/etc....
attr_accessor :escaped_id
# The name of attribute that will be used for template expandion by amrita.
# You will need to set this if you use _id_ attribute fom DOM.
# For expample, if this was set to "amrita_id",
# you can use amrita_id for amrita and id for DOM.
attr_accessor :amrita_id
# If set, use REXML-based parser instead of Amrita's own html-parser
attr_accessor :xml
# If set, the output is an xhtml document.
attr_accessor :asxml
# If Set, use pre_format method
attr_accessor :pre_format
# If set, expand attribute which value is "@xxxx"
attr_accessor :expand_attr
# If Set, use compiler.
attr_accessor :use_compiler
attr_accessor :cache_manager
# debug compiler
attr_accessor :debug_compiler
# The source code that generated by template compiler
attr_reader :src
attr_reader :template
def initialize
@hint = nil
@template = nil
@xml = @prettyprint = @compact_space = @asxml = @pre_format = @expand_attr= false
@keep_id = false
@escaped_id = @dom_id = @amrita_id = nil
@parser_filter = nil
@use_compiler = false
@cache_manager = DummyCacheManager.new
@debug_compiler = false
end
#
# 1. load template if it was changed
#
# 2. compile template if +use_compiler+ was set.
#
# 3. expand template with +model+
#
# 4. print template to +stream+
#
def expand(stream, model)
setup_template if need_update?
context = setup_context
formatter = setup_formatter(stream)
do_expand(model, context, formatter)
end
# set Hint data (undocumented now) and compile template by it.
def set_hint(hint)
@hint = hint
compile_template if @use_compiler
end
# generate Hint from data and compile template by it.
def set_hint_by_sample_data(data)
hint = data.amrita_generate_hint
set_hint(hint)
end
private
def do_expand(model, context, formatter)
if @use_compiler and @compiled_template
begin
#puts "use compiled_template"
@compiled_template::expand(formatter, model, context)
rescue RuntimeError, NameError, ScriptError
if @debug_compiler
puts src
end
raise
end
else
tree = @template.expand(model, context)
formatter.format(tree)
end
end
# setup ExpandContext
def setup_context
context = Amrita::DefaultContext.clone
context.delete_id = false if keep_id
context.expand_attr = expand_attr
context
end
# setup Formatter
def setup_formatter(stream="")
if prettyprint
raise "can't set prettyprint and compact_space both" if compact_space
raise "can't set prettyprint and use_compiler both" if use_compiler
formatter_cls = PrettyPrintFormatter
elsif compact_space
formatter_cls = SingleLineFormatter
else
formatter_cls = AsIsFormatter
end
f = formatter_cls.new(stream, setup_taginfo)
f.asxml = asxml
if escaped_id
raise "can't set escaped_id and keep_id" if keep_id
f.set_attr_filter(escaped_id.intern=>:id)
end
f
end
def setup_taginfo
DefaultHtmlTagInfo
end
def setup_parser_filter
if amrita_id
self.escaped_id = :__id__
self.keep_id = false
@parser_filter = proc do |e|
real_id_val = e[:id]
amrita_id_val = e[amrita_id]
if real_id_val
e[escaped_id] = real_id_val
e.delete_attr!(:id)
end
if amrita_id_val
e[:id] = amrita_id_val
e.delete_attr!(amrita_id)
end
e
end
end
end
def setup_template
@template = @compiled_template = nil
setup_parser_filter
load_template
if @pre_format
f = setup_formatter
@template = @template.pre_format(f).result_as_top
end
if @use_compiler
compile_template
end
end
def compile_template
return if prettyprint
@compiled_template = @cache_manager.cache(cache_path, :module, source_mtime) do
@src = @cache_manager.cache(cache_path, :source, source_mtime) do
formatter = setup_formatter
c = HtmlCompiler::Compiler.new(formatter)
c.delete_id = false if keep_id
c.expand_attr = expand_attr
c.debug_compiler = debug_compiler
c.compile(@template, (@hint or HtmlCompiler::AnyData.new))
c.get_result.join("\n")
end
mod = Module.new
mod.module_eval @src.untaint
mod
end
end
def cache_path
nil # subclass resposibility
end
def source_mtime
nil
end
def get_parser_class
if @xml
require 'amrita/xml'
Amrita::XMLParser
else
Amrita::HtmlParser
end
end
def need_update?
not @template
end
end
class TemplateFile < Template
def initialize(path)
super()
@path = path
@lastread = nil
end
# template will be loaded again if modified.
def need_update?
return true unless @lastread
@lastread < File::stat(@path).mtime
end
def load_template
@template = get_parser_class.parse_file(@path, setup_taginfo) do |e|
if @parser_filter
@parser_filter.call(e)
else
e
end
end
@lastread = Time.now
end
end
class ModuleCache
include CacheManager
def initialize
@hash = {}
end
def get_item(typ, filename, key)
return nil unless typ == :module
@hash[filename.to_s + key.to_s]
end
def save_item(item)
@hash[item.filename.to_s + item.key.to_s] = item
end
end
class SourceCache
include CacheManager
def initialize(dir)
@dir = dir
@module_cache = ModuleCache.new
end
def get_item(typ, filename, key)
case typ
when :module
@module_cache.get_item(typ, filename, key)
when :source
load_source(filename, key)
else
raise "can't happen wrong type #{typ}"
end
end
def save_item(item)
case item.type
when :module
@module_cache.save_item(item)
when :source
save_source(item)
else
raise "can't happen"
end
end
private
def make_cache_path(filename, key)
base = key.to_s + filename.to_s
base.gsub!("/", "_")
File::join(@dir, base)
end
def load_source(filename, key)
item = Item.new(:source, filename, key)
path = make_cache_path(filename, key)
File::open(path) do |f|
item.mtime = f.mtime
item.contents = f.read
end
item
rescue Errno::ENOENT, Errno::EACCES
nil
end
def save_source(item)
path = make_cache_path(item.filename, item.key)
File::open(path, "w") do |f|
f.write item.contents
end
end
end
class TemplateFileWithCache < TemplateFile
# CAUTION: be careful to prevent users to edit the cache file.
# It's *YOUR* resposibility to protect the cache files from
# crackers. Don't use TemplateFileWithCache::set_cache_dir if
# you don't understand this.
def TemplateFileWithCache::set_cache_dir(path)
if path
@@cache_manager = SourceCache.new(path)
else
@@cache_manager = nil
end
end
@@cache_manager = nil
TemplateFileWithCache::set_cache_dir(ENV["AmritaCacheDir"].untaint) # be careful whether this directory is safe
def TemplateFileWithCache::[](path)
TemplateFileWithCache.new(path)
end
def initialize(path)
super
@cache_manager = @@cache_manager if @@cache_manager
end
def cache_path
@path
end
def source_mtime
File::stat(@path).mtime
end
end
class TemplateText < Template
def initialize(template_text, fname="", lno=0)
super()
@template_text, @fname, @lno = template_text, fname, lno
@template = nil
end
def load_template
@template = get_parser_class.parse_text(@template_text, @fname, @lno, setup_taginfo) do |e|
if @parser_filter
@parser_filter.call(e)
else
e
end
end
end
def need_update?
@template == nil
end
end
end