class Template

note: HtmlTemplate is a class (NOT a module) for now - change - why? why not?

Constants

ALL_RE
CATCH_CLOSE_RE
CATCH_OPEN_RE
ELSE_RE
ESCAPES
IDENT
IF_CLOSE_RE
IF_OPEN_RE
LOOP_CLOSE_RE
LOOP_OPEN_RE
MAJOR
MINOR
PATCH
VAR_RE
VERSION

Attributes

errors[R]
names[R]
template[R]
text[R]

Public Class Methods

banner() click to toggle source
config() click to toggle source
# File lib/html/template.rb, line 122
def self.config
  @config ||= Configuration.new
end
configure() { |config| ... } click to toggle source

lets you use

HtmlTemplate.configure do |config|
   config.debug        = true
   config.strict       = true
end
# File lib/html/template.rb, line 118
def self.configure
  yield( config )
end
new( text=nil, filename: nil, strict: config.strict?, loop_vars: config.loop_vars? ) click to toggle source
# File lib/html/template.rb, line 139
def initialize( text=nil, filename: nil, strict: config.strict?, loop_vars: config.loop_vars? )
  if text.nil?   ## try to read file (by filename)
    text = File.open( filename, 'r:utf-8' ) { |f| f.read }
  end

  ## options
  @strict    = strict
  @loop_vars = loop_vars

  ## todo/fix: add filename to ERB too (for better error reporting)
  @text, @errors, @names = convert( text )    ## note: keep a copy of the converted template text

  if @errors.size > 0
    puts "!! ERROR - #{@errors.size} conversion / syntax error(s):"
    pp @errors

    raise     if strict? ## todo - find a good Error - StandardError - why? why not?
  end

  @template      = ERB.new( @text, nil, '%<>' )
end
root() click to toggle source
# File lib/html/template/version.rb, line 19
def self.root
  File.expand_path( File.dirname(File.dirname(File.dirname(File.dirname(__FILE__)))) )
end
version() click to toggle source
# File lib/html/template/version.rb, line 10
def self.version
  VERSION
end

Public Instance Methods

config() click to toggle source

config convenience (shortcut) helpers

# File lib/html/template.rb, line 133
def config() self.class.config; end
convert( text ) click to toggle source
# File lib/html/template.rb, line 274
def convert( text )
  errors = []          # note: reset global errros list
  names  = Names.new   ## keep track of all referenced / used names in VAR/IF/UNLESS/LOOP/etc.

  stack = []

  ## note: convert line-by-line
  ##   allows comments and line no reporting etc.
  buf = String.new('')  ## note: '' required for getting source encoding AND not ASCII-8BIT!!!
  lineno = 0
  text.each_line do |line|
    lineno += 1

    if line.lstrip.start_with?( '#' )    ## or make it tripple ### - why? why not?
       buf << "%#{line.lstrip}"
    elsif line.strip.empty?
       buf << line
    else
       buf << line.gsub( ALL_RE ) do |_|
                m = $~    ## (global) last match object

                tag         = m[:tag]
                tag_open    = m[:open]
                tag_close   = m[:close]

                ident       = m[:ident]
                unknown     = m[:unknown]  # catch all for unknown / unmatched tags

                escape      = m[:escape]
                format      = m[:format]

                ## todo/fix: rename ctx to scope or __ - why? why not?
                ## note: peek; get top stack item
                ##   if top level (stack empty)  => nothing
                ##       otherwise               => channel. or item. etc. (with trailing dot included!)
                ctx = if stack.empty?
                        ''
                      else
                         ## check for special loop variables
                         if loop_vars? &&
                            ['__INDEX__',
                             '__COUNTER__',
                             '__INDEX__',
                             '__COUNTER__',
                             '__ODD__',
                             '__EVEN__',
                             '__FIRST__',
                             '__INNER__',
                             '__OUTER__',
                             '__LAST__'
                            ].include?( ident )
                           "#{ident_to_loop_it( stack[-1] )}_loop."
                         else
                         ## assume plural ident e.g. channels
                         ##  cut-off last char, that is,
                         ##   the plural s channels => channel
                         ##  note:  ALWAYS downcase (auto-generated) loop iterator/pass name
                           "#{ident_to_loop_it( stack[-1] )}."
                         end
                      end

                code = if tag == 'VAR'
                         names.add( stack, ident, '$VAR' )

                         if escape && format == 'HTML'
                             ## check or use long form e.g. CGI.escapeHTML - why? why not?
                            "<%=h #{ctx}#{ident} %>"
                         else
                            "<%= #{ctx}#{ident} %>"
                         end
                       elsif tag == 'LOOP' && tag_open
                         names.add( stack, ident, '$LOOP' )

                         ## assume plural ident e.g. channels
                         ##  cut-off last char, that is, the plural s channels => channel
                         ##  note:  ALWAYS downcase (auto-generated) loop iterator/pass name
                         it = ident_to_loop_it( ident )
                         stack.push( ident )
                         if loop_vars?
                           "<% #{ctx}#{ident}.each_with_loop do |#{it}, #{it}_loop| %>"
                         else
                           "<% #{ctx}#{ident}.each do |#{it}| %>"
                         end
                       elsif tag == 'LOOP' && tag_close
                         stack.pop
                         "<% end %>"
                       elsif tag == 'IF' && tag_open
                         names.add( stack, ident, '$IF' )
                         "<% if #{ctx}#{ident} %>"
                       elsif tag == 'UNLESS' && tag_open
                         names.add( stack, ident, '$UNLESS' )
                         "<% unless #{ctx}#{ident} %>"
                       elsif (tag == 'IF' || tag == 'UNLESS') && tag_close
                         "<% end %>"
                       elsif tag == 'ELSE'
                         "<% else %>"
                       elsif unknown
                          errors <<   if tag_open
                                        "line #{lineno} - unknown open tag: #{unknown}"
                                      else ## assume tag_close
                                        "line #{lineno} - unknown close tag: #{unknown}"
                                      end

                          puts "!! ERROR in line #{lineno} - #{errors[-1]}:"
                          puts line
                          "<%# !!error - #{errors[-1]} %>"
                       else
                         raise ArgumentError  ## unknown tag #{tag}
                       end

                puts " line #{lineno} - match #{m[0]} replacing with: #{code}"  if debug?
                code

              end
      end
    end # each_line
  [buf, errors, names.to_h]
end
debug?() click to toggle source
# File lib/html/template.rb, line 134
def debug?() config.debug?;     end
ident_to_loop_it( ident ) click to toggle source
# File lib/html/template.rb, line 265
def ident_to_loop_it( ident )  # make loop iterator (e.g. Channels => channel and so on)
   ## assume plural ident e.g. channels
   ##  cut-off last char, that is,
   ##   the plural s channels => channel
   ##  note:  ALWAYS downcase (auto-generated) loop iterator/pass name
   ident[0..-2].downcase
end
loop_vars?() click to toggle source
# File lib/html/template.rb, line 137
def loop_vars?() @loop_vars; end
render( **kwargs ) click to toggle source
# File lib/html/template.rb, line 403
def render( **kwargs )
  ## todo: use locals / assigns or something instead of **kwargs - why? why not?
  ##        allow/support (extra) locals / assigns - why? why not?
    ## note: Ruby >= 2.5 has ERB#result_with_hash - use later - why? why not?

  kwargs = kwargs.reduce( {} ) do |hash, (key, val)|
                                 ## puts "#{key} => #{val}:#{val.class.name}"
                                 hash[key] = to_recursive_ostruct( val )
                                 hash
                               end

  ## (auto-)convert array and hash values to ostruct
  ##   for easy dot (.) access
  ##      e.g. student.name instead of student[:name]

  @template.result( Context.new( **kwargs ).get_binding )
end
strict?() click to toggle source
# File lib/html/template.rb, line 136
def strict?()    @strict;    end
to_recursive_ostruct( o ) click to toggle source
# File lib/html/template.rb, line 217
def to_recursive_ostruct( o )
  if o.is_a?( Array )
    o.reduce( [] ) do |ary, item|
                     ary << to_recursive_ostruct( item )
                     ary
                   end
  elsif o.is_a?( Hash )
    ## puts 'to_recursive_ostruct (hash):'
    OpenStruct.new( o.reduce( {} ) do |hash, (key, val)|
                                     ## puts "#{key} => #{val}:#{val.class.name}"
                                     hash[key] = to_recursive_ostruct( val )
                                     hash
                                   end )
  else  ## assume regular "primitive" value - pass along as is
    o
  end
end