cutaneous 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Gemfile +3 -0
- data/LICENSE +0 -0
- data/README.md +0 -0
- data/Rakefile +150 -0
- data/cutaneous.gemspec +103 -0
- data/lib/cutaneous/compiler/expression.rb +85 -0
- data/lib/cutaneous/compiler.rb +223 -0
- data/lib/cutaneous/context.rb +70 -0
- data/lib/cutaneous/engine.rb +66 -0
- data/lib/cutaneous/lexer.rb +92 -0
- data/lib/cutaneous/loader.rb +147 -0
- data/lib/cutaneous/syntax.rb +39 -0
- data/lib/cutaneous/template.rb +40 -0
- data/lib/cutaneous.rb +23 -0
- data/test/fixtures/a.html.cut +13 -0
- data/test/fixtures/b.html.cut +8 -0
- data/test/fixtures/c.html.cut +8 -0
- data/test/fixtures/comments.html.cut +1 -0
- data/test/fixtures/d.html.cut +8 -0
- data/test/fixtures/e.html.cut +4 -0
- data/test/fixtures/error.html.cut +30 -0
- data/test/fixtures/expressions.html.cut +1 -0
- data/test/fixtures/include.html.cut +3 -0
- data/test/fixtures/include.rss.cut +3 -0
- data/test/fixtures/included_error.html.cut +1 -0
- data/test/fixtures/instance.html.cut +2 -0
- data/test/fixtures/instance_include.html.cut +1 -0
- data/test/fixtures/missing.html.cut +1 -0
- data/test/fixtures/other/different.html.cut +1 -0
- data/test/fixtures/other/error.html.cut +5 -0
- data/test/fixtures/partial.html.cut +1 -0
- data/test/fixtures/partial.rss.cut +1 -0
- data/test/fixtures/render.html.cut +6 -0
- data/test/fixtures/statements.html.cut +3 -0
- data/test/fixtures/target.html.cut +1 -0
- data/test/fixtures/whitespace.html.cut +6 -0
- data/test/helper.rb +18 -0
- data/test/test_blocks.rb +19 -0
- data/test/test_cache.rb +104 -0
- data/test/test_core.rb +168 -0
- metadata +90 -0
| @@ -0,0 +1,92 @@ | |
| 1 | 
            +
            # encoding: UTF-8
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'strscan'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module Cutaneous
         | 
| 6 | 
            +
              class Lexer
         | 
| 7 | 
            +
                attr_reader :template, :syntax
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                def initialize(template, syntax)
         | 
| 10 | 
            +
                  @template, @syntax = template, syntax
         | 
| 11 | 
            +
                end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                def tokens
         | 
| 14 | 
            +
                  @tokens ||= parse
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                # def script
         | 
| 18 | 
            +
                #   @script ||= compile
         | 
| 19 | 
            +
                # end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                protected
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                BRACES ||= /\{|\}/
         | 
| 24 | 
            +
                STRIP_WS = "-"
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                def parse
         | 
| 27 | 
            +
                  tokens    = []
         | 
| 28 | 
            +
                  scanner   = StringScanner.new(@template.to_s)
         | 
| 29 | 
            +
                  tag_start = syntax.tag_start_pattern
         | 
| 30 | 
            +
                  tags      = syntax.tags
         | 
| 31 | 
            +
                  token_map = syntax.token_map
         | 
| 32 | 
            +
                  previous  = nil
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                  while (text = scanner.scan_until(tag_start))
         | 
| 35 | 
            +
                    tag = scanner.matched
         | 
| 36 | 
            +
                    type, brace_count, endtag_length = token_map[tag]
         | 
| 37 | 
            +
                    text.slice!(text.length - tag.length, text.length)
         | 
| 38 | 
            +
                    expression = ""
         | 
| 39 | 
            +
                    strip_whitespace = false
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                    begin
         | 
| 42 | 
            +
                      expression << scanner.scan_until(BRACES)
         | 
| 43 | 
            +
                      brace = scanner.matched
         | 
| 44 | 
            +
                      brace_count += ((123 - brace.ord)+1) # '{' = 1,  '}' = -1
         | 
| 45 | 
            +
                    end while (brace_count > 0)
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                    length = expression.length
         | 
| 48 | 
            +
                    expression.slice!(length - endtag_length, length)
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                    if expression.end_with?(STRIP_WS)
         | 
| 51 | 
            +
                      strip_whitespace = true
         | 
| 52 | 
            +
                      length = expression.length
         | 
| 53 | 
            +
                      expression.slice!(length - 1, length)
         | 
| 54 | 
            +
                    end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                    tokens << place_text_token(text) if text.length > 0
         | 
| 57 | 
            +
                    tokens << create_token(type, expression, strip_whitespace)
         | 
| 58 | 
            +
                    previous = type
         | 
| 59 | 
            +
                  end
         | 
| 60 | 
            +
                  tokens << place_text_token(scanner.rest) unless scanner.eos?
         | 
| 61 | 
            +
                  tokens
         | 
| 62 | 
            +
                end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                def create_token(type, expression, strip_whitespace)
         | 
| 65 | 
            +
                  [type, expression, strip_whitespace]
         | 
| 66 | 
            +
                end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                #BEGINNING_WHITESPACE ||= /\A\s*?[\r\n]+/
         | 
| 69 | 
            +
                #ENDING_WHITESPACE    ||= /(\r?\n)[ \t]*\z/
         | 
| 70 | 
            +
                ESCAPE_STRING        ||= /[`\\]/
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                def place_text_token(expression)
         | 
| 73 | 
            +
                  expression.gsub!(syntax.escaped_tag_pattern, '\1')
         | 
| 74 | 
            +
                  expression.gsub!(ESCAPE_STRING, '\\\\\&')
         | 
| 75 | 
            +
                  [:text, expression]
         | 
| 76 | 
            +
                end
         | 
| 77 | 
            +
              end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
              FirstPassSyntax = Cutaneous::Syntax.new({
         | 
| 80 | 
            +
                :comment => %w(!{ }),
         | 
| 81 | 
            +
                :expression => %w(${ }),
         | 
| 82 | 
            +
                :escaped_expression => %w($${ }),
         | 
| 83 | 
            +
                :statement => %w(%{ })
         | 
| 84 | 
            +
              })
         | 
| 85 | 
            +
             | 
| 86 | 
            +
              SecondPassSyntax = Cutaneous::Syntax.new({
         | 
| 87 | 
            +
                :comment => %w(!{ }),
         | 
| 88 | 
            +
                :expression => %w({{ }}),
         | 
| 89 | 
            +
                :escaped_expression => %w({$ $}),
         | 
| 90 | 
            +
                :statement => %w({% %})
         | 
| 91 | 
            +
              })
         | 
| 92 | 
            +
            end
         | 
| @@ -0,0 +1,147 @@ | |
| 1 | 
            +
            module Cutaneous
         | 
| 2 | 
            +
              # Converts a template path or Proc into a Template instance for a particular format
         | 
| 3 | 
            +
              class FileLoader
         | 
| 4 | 
            +
                attr_accessor :syntax
         | 
| 5 | 
            +
                attr_writer   :template_class
         | 
| 6 | 
            +
                attr_reader   :format
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                def initialize(template_roots, format, extension = Cutaneous.extension)
         | 
| 9 | 
            +
                  @roots, @format, @extension = template_roots, format, extension
         | 
| 10 | 
            +
                  @template_class = Template
         | 
| 11 | 
            +
                end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                def render(template, context)
         | 
| 14 | 
            +
                  template(template).render(context)
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                def template(template)
         | 
| 18 | 
            +
                  return proc_template(template) if template.is_a?(Proc)
         | 
| 19 | 
            +
                  template_path = path(template)
         | 
| 20 | 
            +
                  raise UnknownTemplateError.new(@roots, filename(template)) if template_path.nil?
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                  @template_class.new(file_lexer(template_path)).tap do |template|
         | 
| 23 | 
            +
                    template.path   = template_path
         | 
| 24 | 
            +
                    template.loader = self
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                def proc_template(lmda)
         | 
| 29 | 
            +
                  StringLoader.new(self).template(lmda.call)
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                def file_lexer(template_path)
         | 
| 33 | 
            +
                  lexer(SourceFile.new(template_path))
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                def lexer(template_string)
         | 
| 37 | 
            +
                  Lexer.new(template_string, syntax)
         | 
| 38 | 
            +
                end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                def path(template_name)
         | 
| 41 | 
            +
                  filename = filename(template_name)
         | 
| 42 | 
            +
                  return filename if ::File.exists?(filename) # Test for an absolute path
         | 
| 43 | 
            +
                  @roots.map { |root| ::File.join(root, filename)}.detect { |path| ::File.exists?(path) }
         | 
| 44 | 
            +
                end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                def filename(template_name)
         | 
| 47 | 
            +
                  [template_name, @format, @extension].join(".")
         | 
| 48 | 
            +
                end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                def exists?(template_root, template_name)
         | 
| 51 | 
            +
                  File.exists?(File.join(template_root, filename(template_name)))
         | 
| 52 | 
            +
                end
         | 
| 53 | 
            +
              end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
              # Converts a template string into a Template instance.
         | 
| 56 | 
            +
              #
         | 
| 57 | 
            +
              # Because a string template can only come from the engine instance
         | 
| 58 | 
            +
              # we need a FileLoader to delegate all future template loading to.
         | 
| 59 | 
            +
              class StringLoader < FileLoader
         | 
| 60 | 
            +
                def initialize(file_loader)
         | 
| 61 | 
            +
                  @file_loader = file_loader
         | 
| 62 | 
            +
                end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                def syntax
         | 
| 65 | 
            +
                  @file_loader.syntax
         | 
| 66 | 
            +
                end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                def template(template_string)
         | 
| 69 | 
            +
                  Template.new(lexer(template_string)).tap do |template|
         | 
| 70 | 
            +
                    template.loader = @file_loader
         | 
| 71 | 
            +
                  end
         | 
| 72 | 
            +
                end
         | 
| 73 | 
            +
              end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
              # Converts a filepath to a template string as and when necessary
         | 
| 76 | 
            +
              class SourceFile
         | 
| 77 | 
            +
                attr_reader :path
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                def initialize(filepath)
         | 
| 80 | 
            +
                  @path = filepath
         | 
| 81 | 
            +
                end
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                def to_s
         | 
| 84 | 
            +
                  File.read(@path)
         | 
| 85 | 
            +
                end
         | 
| 86 | 
            +
              end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
              # Caches Template instances
         | 
| 89 | 
            +
              class CachedFileLoader < FileLoader
         | 
| 90 | 
            +
                def initialize(template_roots, format, extension = Cutaneous.extension)
         | 
| 91 | 
            +
                  super
         | 
| 92 | 
            +
                  @template_class = CachedTemplate
         | 
| 93 | 
            +
                end
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                def template_cache
         | 
| 96 | 
            +
                  @template_cache ||= {}
         | 
| 97 | 
            +
                end
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                def write_compiled_scripts=(flag)
         | 
| 100 | 
            +
                  if flag
         | 
| 101 | 
            +
                    @template_class = CachedTemplate
         | 
| 102 | 
            +
                  else
         | 
| 103 | 
            +
                    @template_class = Template
         | 
| 104 | 
            +
                  end
         | 
| 105 | 
            +
                end
         | 
| 106 | 
            +
             | 
| 107 | 
            +
                def template(template)
         | 
| 108 | 
            +
                  return template_cache[template] if template_cache.key?(template)
         | 
| 109 | 
            +
                  template_cache[template] = super
         | 
| 110 | 
            +
                end
         | 
| 111 | 
            +
              end
         | 
| 112 | 
            +
             | 
| 113 | 
            +
              # Provides an additional caching mechanism by writing generated template
         | 
| 114 | 
            +
              # scripts to a .rb file.
         | 
| 115 | 
            +
              class CachedTemplate < Template
         | 
| 116 | 
            +
             | 
| 117 | 
            +
                def script
         | 
| 118 | 
            +
                  if cached?
         | 
| 119 | 
            +
                    script = File.read(script_path)
         | 
| 120 | 
            +
                  else
         | 
| 121 | 
            +
                    script = super
         | 
| 122 | 
            +
                    File.open(script_path, "w") do |f|
         | 
| 123 | 
            +
                      f.write(script)
         | 
| 124 | 
            +
                    end
         | 
| 125 | 
            +
                  end
         | 
| 126 | 
            +
                  script
         | 
| 127 | 
            +
                end
         | 
| 128 | 
            +
             | 
| 129 | 
            +
                def cached?
         | 
| 130 | 
            +
                  File.exist?(script_path) && (File.mtime(script_path) >= File.mtime(template_path))
         | 
| 131 | 
            +
                end
         | 
| 132 | 
            +
             | 
| 133 | 
            +
                def template_path
         | 
| 134 | 
            +
                  lexer.template.path
         | 
| 135 | 
            +
                end
         | 
| 136 | 
            +
             | 
| 137 | 
            +
                def script_path
         | 
| 138 | 
            +
                  @source_path ||= generate_script_path
         | 
| 139 | 
            +
                end
         | 
| 140 | 
            +
             | 
| 141 | 
            +
                def generate_script_path
         | 
| 142 | 
            +
                  path = template_path
         | 
| 143 | 
            +
                  ext  = File.extname path
         | 
| 144 | 
            +
                  path.gsub(/#{ext}$/, ".rb")
         | 
| 145 | 
            +
                end
         | 
| 146 | 
            +
              end
         | 
| 147 | 
            +
            end
         | 
| @@ -0,0 +1,39 @@ | |
| 1 | 
            +
            # encoding: UTF-8
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Cutaneous
         | 
| 4 | 
            +
              class Syntax
         | 
| 5 | 
            +
                attr_reader :tags
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                def initialize(tag_definitions)
         | 
| 8 | 
            +
                  @tags = tag_definitions
         | 
| 9 | 
            +
                end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                def is_dynamic?(text)
         | 
| 12 | 
            +
                  !text.index(tag_start_pattern).nil?
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                def tag_start_pattern
         | 
| 16 | 
            +
                  @tag_start_pattern ||= compile_start_pattern
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                def escaped_tag_pattern
         | 
| 20 | 
            +
                  @escaped_tag_pattern ||= compile_start_pattern_with_prefix("\\\\")
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                def compile_start_pattern
         | 
| 24 | 
            +
                  not_escaped = "(?<!\\\\)"
         | 
| 25 | 
            +
                  compile_start_pattern_with_prefix(not_escaped)
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                def compile_start_pattern_with_prefix(prefix)
         | 
| 29 | 
            +
                  openings = self.tags.map { |type, tags| Regexp.escape(tags[0]) }
         | 
| 30 | 
            +
                  Regexp.new("#{prefix}(#{ openings.join("|") })")
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                # map the set of tags into a hash used by the parse routine that converts an opening tag into a
         | 
| 34 | 
            +
                # list of: tag type, the number of opening braces in the tag and the length of the closing tag
         | 
| 35 | 
            +
                def token_map
         | 
| 36 | 
            +
                  @token_map ||= Hash[tags.map { |type, tags| [tags[0], [type, tags[0].count(?{), tags[1].length]] }]
         | 
| 37 | 
            +
                end
         | 
| 38 | 
            +
              end
         | 
| 39 | 
            +
            end
         | 
| @@ -0,0 +1,40 @@ | |
| 1 | 
            +
            module Cutaneous
         | 
| 2 | 
            +
              class Template
         | 
| 3 | 
            +
                attr_accessor :loader, :lexer, :path
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                def initialize(lexer)
         | 
| 6 | 
            +
                  @lexer = lexer
         | 
| 7 | 
            +
                end
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                def compiler
         | 
| 10 | 
            +
                  @compiler ||= Compiler.new(lexer, loader)
         | 
| 11 | 
            +
                end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                def render(context)
         | 
| 14 | 
            +
                  context.__loader = loader
         | 
| 15 | 
            +
                  context.instance_eval(&template_proc)
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                def template_proc
         | 
| 19 | 
            +
                  @template_proc ||= eval(template_proc_src, nil, path || "(cutaneous)").tap do |proc|
         | 
| 20 | 
            +
                    @lexer = nil # release any memory used by the lexer, we don't need it anymore
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                def template_proc_src
         | 
| 25 | 
            +
                  "lambda { |context| self.__buf = __buf = ''; #{script}; __buf.to_s }"
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                def script
         | 
| 29 | 
            +
                  compiler.script
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                def block_order
         | 
| 33 | 
            +
                  compiler.block_order
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                def block(block_name)
         | 
| 37 | 
            +
                  compiler.block(block_name)
         | 
| 38 | 
            +
                end
         | 
| 39 | 
            +
              end
         | 
| 40 | 
            +
            end
         | 
    
        data/lib/cutaneous.rb
    ADDED
    
    | @@ -0,0 +1,23 @@ | |
| 1 | 
            +
            require 'cutaneous/engine'
         | 
| 2 | 
            +
            require 'cutaneous/template'
         | 
| 3 | 
            +
            require 'cutaneous/loader'
         | 
| 4 | 
            +
            require 'cutaneous/context'
         | 
| 5 | 
            +
            require 'cutaneous/syntax'
         | 
| 6 | 
            +
            require 'cutaneous/lexer'
         | 
| 7 | 
            +
            require 'cutaneous/compiler'
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            module Cutaneous
         | 
| 10 | 
            +
              VERSION = "0.1.0"
         | 
| 11 | 
            +
             | 
| 12 | 
            +
              class CompilationError < Exception; end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
              class UnknownTemplateError < Exception
         | 
| 15 | 
            +
                def initialize(template_roots, relative_path)
         | 
| 16 | 
            +
                  super("Template '#{relative_path}' not found under #{template_roots.inspect}")
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
              end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
              def self.extension
         | 
| 21 | 
            +
                "cut"
         | 
| 22 | 
            +
              end
         | 
| 23 | 
            +
            end
         | 
| @@ -0,0 +1 @@ | |
| 1 | 
            +
            !{ this is a comment }
         | 
| @@ -0,0 +1,30 @@ | |
| 1 | 
            +
             | 
| 2 | 
            +
             | 
| 3 | 
            +
            %{
         | 
| 4 | 
            +
            	3.times do |n|
         | 
| 5 | 
            +
            }
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            !{
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            	comment
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            	}
         | 
| 12 | 
            +
            ${ n -}
         | 
| 13 | 
            +
            %{
         | 
| 14 | 
            +
            	end }
         | 
| 15 | 
            +
            %{ include "partial" -}
         | 
| 16 | 
            +
            %{ test = "here"
         | 
| 17 | 
            +
             | 
| 18 | 
            +
            -}
         | 
| 19 | 
            +
             | 
| 20 | 
            +
             | 
| 21 | 
            +
             | 
| 22 | 
            +
             | 
| 23 | 
            +
            here ${ test }
         | 
| 24 | 
            +
             | 
| 25 | 
            +
            !{
         | 
| 26 | 
            +
            	the error message should be the same as the line number of the expression
         | 
| 27 | 
            +
            	}
         | 
| 28 | 
            +
            %{
         | 
| 29 | 
            +
            	raise "29"
         | 
| 30 | 
            +
            }
         | 
| @@ -0,0 +1 @@ | |
| 1 | 
            +
            This is ${ right } $${ code }
         | 
| @@ -0,0 +1 @@ | |
| 1 | 
            +
            %{ include "other/error" }
         | 
| @@ -0,0 +1 @@ | |
| 1 | 
            +
            ${ @right } = ${ right }
         | 
| @@ -0,0 +1 @@ | |
| 1 | 
            +
            missing: ${ unknown }
         | 
| @@ -0,0 +1 @@ | |
| 1 | 
            +
            ${ right }
         | 
| @@ -0,0 +1 @@ | |
| 1 | 
            +
            right = ${ right }
         | 
| @@ -0,0 +1 @@ | |
| 1 | 
            +
            ${ right } = rss
         | 
| @@ -0,0 +1 @@ | |
| 1 | 
            +
            ${ name }
         | 
    
        data/test/helper.rb
    ADDED
    
    | @@ -0,0 +1,18 @@ | |
| 1 | 
            +
            $:.push File.expand_path("../../lib", __FILE__)
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'minitest/spec'
         | 
| 4 | 
            +
            require 'minitest/autorun'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            require 'cutaneous'
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            class TestContext < Cutaneous::Context
         | 
| 9 | 
            +
              def escape(value)
         | 
| 10 | 
            +
                value.gsub(/</, "<").gsub(/>/, ">")
         | 
| 11 | 
            +
              end
         | 
| 12 | 
            +
            end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            class MiniTest::Spec
         | 
| 15 | 
            +
              def ContextHash(params = {})
         | 
| 16 | 
            +
                TestContext.new(Object.new, params)
         | 
| 17 | 
            +
              end
         | 
| 18 | 
            +
            end
         | 
    
        data/test/test_blocks.rb
    ADDED
    
    | @@ -0,0 +1,19 @@ | |
| 1 | 
            +
            require File.expand_path('../helper', __FILE__)
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            describe Cutaneous do
         | 
| 4 | 
            +
              let(:template_root) { File.expand_path("../fixtures", __FILE__)                     }
         | 
| 5 | 
            +
              let(:engine)        { Cutaneous::Engine.new(template_root, Cutaneous::FirstPassSyntax, "html") }
         | 
| 6 | 
            +
             | 
| 7 | 
            +
              it "Will parse & execute a simple template with expressions" do
         | 
| 8 | 
            +
                context = ContextHash(right: "right", code: "<tag/>")
         | 
| 9 | 
            +
                result = engine.render("c", context)
         | 
| 10 | 
            +
                expected = ["aa\n\n", "ab", "bb", "cb", "ac", "ad", "ae", "cf", "ag\n"].join("\n\n")
         | 
| 11 | 
            +
                result.must_equal expected
         | 
| 12 | 
            +
              end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
              it "Won't run code in inherited templates unless called" do
         | 
| 15 | 
            +
                context = ContextHash(right: "right", code: "<tag/>")
         | 
| 16 | 
            +
                result = engine.render("e", context)
         | 
| 17 | 
            +
                result.must_equal ["da", "db", "dc", "ed\n\n"].join("\n\n")
         | 
| 18 | 
            +
              end
         | 
| 19 | 
            +
            end
         | 
    
        data/test/test_cache.rb
    ADDED
    
    | @@ -0,0 +1,104 @@ | |
| 1 | 
            +
            require File.expand_path('../helper', __FILE__)
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'tmpdir'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            describe Cutaneous do
         | 
| 6 | 
            +
              let(:source_template_root) { File.expand_path("../fixtures", __FILE__) }
         | 
| 7 | 
            +
              let(:dest_template_root)   { Dir.mktmpdir }
         | 
| 8 | 
            +
              let(:engine)               { cached_engine }
         | 
| 9 | 
            +
             | 
| 10 | 
            +
              def cached_engine
         | 
| 11 | 
            +
                Cutaneous::CachingEngine.new(dest_template_root, Cutaneous::FirstPassSyntax, "html")
         | 
| 12 | 
            +
              end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
              def template(source, format = "html")
         | 
| 15 | 
            +
                dest_path = template_path(source, format)
         | 
| 16 | 
            +
                FileUtils.mkdir_p(File.dirname(dest_path))
         | 
| 17 | 
            +
                FileUtils.cp(File.join(source_template_root, File.basename(dest_path)), dest_path)
         | 
| 18 | 
            +
                source
         | 
| 19 | 
            +
              end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
              def remove_template(source, format = "html")
         | 
| 22 | 
            +
                FileUtils.rm(template_path(source, format))
         | 
| 23 | 
            +
              end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
              def template_path(source, format = "html", extension = "cut")
         | 
| 26 | 
            +
                filename =  "#{source}.#{format}.#{extension}"
         | 
| 27 | 
            +
                File.join(dest_template_root, filename)
         | 
| 28 | 
            +
              end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
              it "Reads templates from the cache if they have been used before" do
         | 
| 31 | 
            +
                templates = %w(a b c)
         | 
| 32 | 
            +
                context = ContextHash(right: "right")
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                templates.each do |t|
         | 
| 35 | 
            +
                  template(t)
         | 
| 36 | 
            +
                end
         | 
| 37 | 
            +
                result1 = engine.render("c", context)
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                templates.each do |t|
         | 
| 40 | 
            +
                  remove_template(t)
         | 
| 41 | 
            +
                end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                result2 = engine.render("c", context)
         | 
| 44 | 
            +
                result2.must_equal result1
         | 
| 45 | 
            +
              end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
              it "Saves the ruby script as a .rb file and uses it if present" do
         | 
| 48 | 
            +
                templates = %w(a b c)
         | 
| 49 | 
            +
                templates.each do |t|
         | 
| 50 | 
            +
                  template(t)
         | 
| 51 | 
            +
                end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                context = ContextHash(right: "right")
         | 
| 54 | 
            +
                result1 = engine.render("c", context)
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                # Ensure that the cached script file is being used by overwriting its contents
         | 
| 57 | 
            +
                path = template_path("c", "html", "rb")
         | 
| 58 | 
            +
                assert ::File.exists?(path), "Template cache should have created '#{path}'"
         | 
| 59 | 
            +
                File.open(path, "w") do |f|
         | 
| 60 | 
            +
                  f.write("__buf << 'right'")
         | 
| 61 | 
            +
                end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                engine = cached_engine
         | 
| 64 | 
            +
                result2 = engine.render("c", context)
         | 
| 65 | 
            +
                result2.must_equal "right"
         | 
| 66 | 
            +
              end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
              it "Recompiles the cached script if the template is newer" do
         | 
| 69 | 
            +
                templates = %w(a b c)
         | 
| 70 | 
            +
                templates.each do |t|
         | 
| 71 | 
            +
                  template(t)
         | 
| 72 | 
            +
                end
         | 
| 73 | 
            +
                context = ContextHash(right: "right")
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                result1 = engine.render("c", context)
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                now = Time.now
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                template_path = template_path("c", "html")
         | 
| 80 | 
            +
                script_path   = template_path("c", "html", "rb")
         | 
| 81 | 
            +
                assert ::File.exists?(script_path), "Template cache should have created '#{script_path}'"
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                File.open(template_path, "w") { |f| f.write("template") }
         | 
| 84 | 
            +
                File.utime(now, now, template_path)
         | 
| 85 | 
            +
                File.utime(now - 100, now - 100, script_path)
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                engine = cached_engine
         | 
| 88 | 
            +
                result1 = engine.render("c", context)
         | 
| 89 | 
            +
                result1.must_equal "template"
         | 
| 90 | 
            +
              end
         | 
| 91 | 
            +
             | 
| 92 | 
            +
              it "Doesn't write a compiled script file if configured not to do so" do
         | 
| 93 | 
            +
                engine.write_compiled_scripts = false
         | 
| 94 | 
            +
                templates = %w(a b c)
         | 
| 95 | 
            +
                templates.each do |t|
         | 
| 96 | 
            +
                  template(t)
         | 
| 97 | 
            +
                end
         | 
| 98 | 
            +
                context = ContextHash(right: "right")
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                result1 = engine.render("c", context)
         | 
| 101 | 
            +
                script_path   = template_path("c", "html", "rb")
         | 
| 102 | 
            +
                refute ::File.exists?(script_path), "Template cache should not have created '#{script_path}'"
         | 
| 103 | 
            +
              end
         | 
| 104 | 
            +
            end
         |