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