class Eggshell::Processor
Constants
- BACKSLASH_REGEX
- BACKSLASH_UNESCAPE_MAP
- BH
A block handler handles one or more lines as a unit as long as all the lines conform to the block's expectations.
Blocks are either identified explicitly:
pre. block_name. some content \ block_name({}). some content
Or, through some non-alphanumeric character:
pre. |table|start|inferred |another|row|here
When a handler can handle a line, it sets an internal block type (retrieved with {{current_type()}}). Subsequent lines are passed to {{continue_with()}} which returns `true` if the line conforms to the current type or `false` to close the block.
The line or lines are finally passed to {{process()}} to generate the output.
h2.
Block
StandardsWhen explicitly calling a block and passing a parameter, always expect the first argument to be a hash of various attributes:
pre. p({'class': '', 'id': '', 'attributes': {}}, other, arguments, …). paragraph start
- BLOCK_MATCH
- BLOCK_MATCH_PARAMS
- COMMENT
- DIRECTIVE
- PIPE_INLINE
This string in a block indicates that a piped macro's output should be inserted at this location rather than immediately after last line. For now, this is only checked for on the last line.
Multiple inline pipes can be specified on this line, with each pipe corresponding to each macro chained to the block. Any unfilled pipe will be replaced with a blank string.
To escape the pipe, use a backslash anywhere [AFTER] the initial dash (e.g. `-\>*<-`).
- TAB
- TAB_SPACE
Attributes
Public Class Methods
# File lib/eggshell/processor.rb, line 8 def initialize(context = nil) @context = context @context = Eggshell::ProcessorContext.new if !context.is_a?(Eggshell::ProcessorContext) @vars = @context.vars @funcs = @context.funcs @macros = @context.macros @blocks = @context.blocks @blocks_map = @context.blocks_map @block_params = @context.block_params @expr_cache = @context.expr_cache @fmt_handlers = @context.fmt_handlers @ee = Eggshell::ExpressionEvaluator.new(@vars, @funcs) @vars[:include_paths] = [] if !@vars[:include_paths] @vars[:include_paths] << File.realdirpath(Dir.pwd()) @noop_macro = Eggshell::MacroHandler::Defaults::NoOpHandler.new @noop_block = Eggshell::BlockHandler::Defaults::NoOpHandler.new end
Unescapes backslashes and Unicode characters.
If a match is made against {{BACKSLASH_UNESCAPE_MAP}} that character will be used, otherwise, the literal is used.
Unicode sequences are standard Ruby-like syntax: {{uabcd}} or {{u{seq1 seq2 …}}}.
# File lib/eggshell/processor.rb, line 671 def self.unescape(str) str = str.gsub(BACKSLASH_REGEX) do |match| if match.length == 2 c = match[1] BACKSLASH_UNESCAPE_MAP[c] || c else if match[2] == '{' parts = match[3..-1].split(' ') buff = '' parts.each do |part| buff += [part.to_i(16)].pack('U') end buff else [match[2..-1].to_i(16)].pack('U') end end end end
Public Instance Methods
# File lib/eggshell/processor.rb, line 85 def _debug(msg) return if @vars['log.level'] < 2 $stderr.write("[DEBUG] #{msg}\n") end
# File lib/eggshell/processor.rb, line 72 def _error(msg) $stderr.write("[ERROR] #{msg}\n") end
# File lib/eggshell/processor.rb, line 80 def _info(msg) return if @vars['log.level'] < 1 $stderr.write("[INFO] #{msg}\n") end
# File lib/eggshell/processor.rb, line 90 def _trace(msg) return if @vars['log.level'] < 3 $stderr.write("[TRACE] #{msg}\n") end
# File lib/eggshell/processor.rb, line 76 def _warn(msg) $stderr.write("[WARN] #{msg}\n") end
# File lib/eggshell/processor.rb, line 30 def add_block_handler(handler, *names) _trace "add_block_handler: #{names.inspect} -> #{handler.class}" @blocks << handler names.each do |name| @blocks_map[name] = handler end end
Register inline format handlers with opening and closing tags. Typically, tags can be arbitrarily nested. However, nesting can be shut off completely or selectively by specifying 0 or more tags separated by a space (empty string is completely disabled).
@param Array tags Each entry should be a 2- or 3-element array in the following form: {{[open, close[, non_nest]]}} @todo if opening tag is regex, don't escape (but make sure it doesn't contain {{^}} or {{$}})
# File lib/eggshell/processor.rb, line 512 def add_format_handler(handler, tags) return if !tags.is_a?(Array) tags.each do |entry| open, close, no_nest = entry no_nest = '' if no_nest.is_a?(TrueClass) @fmt_handlers[open] = [handler, close, no_nest] _trace "add_format_handler: #{open} #{close} (non-nested: #{no_nest.inspect})" end # regenerate splitting pattern going from longest to shortest openers = @fmt_handlers.keys.sort do |a, b| b.length <=> a.length end regex = '' openers.each do |op| regex = "#{regex}|#{Regexp.quote(op)}|#{Regexp.quote(@fmt_handlers[op][1])}" end @fmt_regex = /(\\|'|"#{regex})/ end
# File lib/eggshell/processor.rb, line 50 def add_macro_handler(handler, *names) _trace "add_macro_handler: #{names.inspect} -> #{handler.class}" names.each do |name| @macros[name] = handler end end
Goes through each item in parse tree, collecting output in the following manner: # {{String}}s and {{Line}}s are outputted as-is # macros and blocks with matching handlers get {{process}} called All output is joined with `\\n` by default. The output object and join string can be overridden through the {{opts}} parameter keys {{:out}} and {{:joiner}}.
3
@param Eggshell::ParseTree,Array parse_tree Parsed document. @param Integer call_depth @param Hash opts
# File lib/eggshell/processor.rb, line 392 def assemble(parse_tree, call_depth = 0, opts = {}) opts = {} if !opts.is_a?(Hash) out = opts[:out] || get_out joiner = opts[:join] || "\n" parse_tree = parse_tree.tree if parse_tree.is_a?(Eggshell::ParseTree) raise Exception.new("input not an array or ParseTree (depth=#{call_depth})") if !parse_tree.is_a?(Array) last_type = nil last_line = 0 last_macro = nil deferred = nil parse_tree.each do |unit| if unit.is_a?(String) out << unit last_line += 1 last_type = nil elsif unit.is_a?(Eggshell::Line) out << unit.to_s last_line = unit.line_nameum last_type = nil elsif unit.is_a?(Array) handler = unit[0] == :block ? @blocks_map[unit[1]] : @macros[unit[1]] name = unit[1] if !handler _warn "handler not found: #{unit[0]} -> #{unit[1]}" next end #$stderr.write "#{unit[0]}:#{unit[1]}\n\t#{unit[2].inspect}\n" args_o = unit[2] || [] args = [] args_o.each do |arg| args << (arg.is_a?(Array) ? @ee.evaluate([arg]) : arg) end lines = unit[ParseTree::IDX_LINES] lines_start = unit[ParseTree::IDX_LINES_START] lines_end = unit[ParseTree::IDX_LINES_END] _handler, _name, _args, _lines = deferred if unit[0] == :block if deferred # two cases: # 1. this block is immediately tied to block-macro chain and is continuation of same type of block # 2. part of block-macro chain but not same type, or immediately follows another block if last_type == :macro && (lines_start - last_line <= 1) && _handler.equal?(handler, name) lines.each do |line| _lines << line end else _handler.process(_name, _args, _lines, out, call_depth) deferred = [handler, name, args, lines.clone] end else deferred = [handler, name, args, lines.clone] end last_line = lines_end else # macro immediately after a block, so assume that output gets piped into last lines # of closest block if deferred && lines_start - last_line == 1 _last = _lines[-1] pinline = false pipe = _lines if _last.to_s.index(PIPE_INLINE) pipe = [] pinline = true end handler.process(name, args, lines, pipe, call_depth) # inline pipe; join output with literal \n to avoid processing lines in block process if pinline if _last.is_a?(Eggshell::Line) _lines[-1] = _last.replace(_last.line.sub(PIPE_INLINE, pipe.join('\n'))) else _lines[-1] = _last.sub(PIPE_INLINE, pipe.join('\n')) end end else if deferred _handler.process(_name, _args, _lines, out, call_depth) deferred = nil end handler.process(name, args, lines, out, call_depth) end last_line = lines_end end last_type = unit[0] elsif unit _warn "not sure how to handle #{unit.class}" _debug unit.inspect last_type = nil end end if deferred _handler, _name, _args, _lines = deferred _handler.process(_name, _args, _lines, out, call_depth) deferred = nil end out.join(joiner) end
Calls inline formatting, expression extrapolator, and backslash unescape.
# File lib/eggshell/processor.rb, line 696 def expand_all(str) unescape(expand_expr(expand_formatting(str))) end
Expands expressions (`${}`) and macro calls (`@@macro@@`). @todo deprecate @@macro@@?
# File lib/eggshell/processor.rb, line 103 def expand_expr(expr) # replace dynamic placeholders # @todo expand to actual expressions buff = [] esc = false exp = false mac = false toks = expr.split(/(\\|\$\{|\}|@@|"|')/) i = 0 plain_str = '' expr_str = '' quote = nil expr_delim = nil while i < toks.length tok = toks[i] i += 1 next if tok == '' if esc plain_str += '\\' + tok esc = false next end if exp if quote expr_str += tok if tok == quote quote = nil end elsif tok == '"' || tok == "'" expr_str += tok quote = tok elsif tok == expr_delim struct = @expr_cache[expr_str] if !struct struct = @ee.parse(expr_str) @expr_cache[expr_str] = struct end if !mac buff << expr_eval(struct) else args = struct[0] macro = args[1] args = args[2] || [] macro_handler = @macros[macro] if macro_handler macro_handler.process(buff, macro, args, nil, -1) else _warn("macro (inline) not found: #{macro}") end end exp = false mac = false expr_delim = nil expr_str = '' else expr_str += tok end # only unescape if not in expression, since expression needs to be given as-is elsif tok == '\\' esc = true next elsif tok == '${' || tok == '@@' if plain_str != '' buff << plain_str plain_str = '' end exp = true expr_delim = '}' if tok == '@@' mac = true expr_delim = tok end else plain_str += tok end end # if exp -- throw exception? buff << plain_str if plain_str != '' return buff.join('') end
Expands inline formatting with {{Eggshell::FormatHandler}}s.
# File lib/eggshell/processor.rb, line 536 def expand_formatting(str) toks = str.gsub(PIPE_INLINE, '').split(@fmt_regex) toks.delete('') buff = [''] quote = nil opened = [] closing = [] non_nesting = [] i = 0 while i < toks.length tok = toks[i] i += 1 if tok == '\\' # preserve escape char otherwise we lose things like \n or \t buff[-1] += tok + toks[i] i += 1 elsif quote quote = nil if tok == quote buff[-1] += tok elsif tok == '"' || tok == "'" # only open quote if there's whitespace or blank string preceeding it quote = tok if opened[-1] && (!buff[-1] || buff[-1] == '' || buff[-1].match(/\s$/)) buff[-1] += tok elsif @fmt_handlers[tok] && (!non_nesting[-1] || non_nesting.index(tok)) handler, closer, non_nest = @fmt_handlers[tok] opened << tok closing << closer non_nesting << non_nest buff << '' elsif tok == closing[-1] opener = opened.pop handler = @fmt_handlers[opener][0] closing.pop non_nesting.pop # @todo insert placeholder and swap out at end? might be a prob if value has to be escaped bstr = buff.pop buff[-1] += handler.format(opener, bstr) else buff[-1] += tok end end opened.each do |op| bstr = buff.pop buff[-1] += op + bstr _warn "expand_formatting: unclosed #{op}, not doing anything: #{bstr}" #_warn toks.inspect end buff.join('') end
# File lib/eggshell/processor.rb, line 97 def expr_eval(struct) return @ee.evaluate(struct) end
# File lib/eggshell/processor.rb, line 38 def get_block_handler(name) @blocks_map[name] end
# File lib/eggshell/processor.rb, line 57 def get_macro_handler(name) @macros[name] end
# File lib/eggshell/processor.rb, line 203 def get_out if !@out [] elsif @out.is_a?(Class) @out.new else @out end end
# File lib/eggshell/processor.rb, line 591 def parse_block_start(line) block_type = nil args = [] bt = line.match(BLOCK_MATCH_PARAMS) if bt idx0 = bt[0].length idx1 = line.index(')', idx0) if idx1 block_type = line[0..idx0-2] params = line[0...idx1+1].strip line = line[idx1+2..line.length] || '' if params != '' struct = @ee.parse(params) args = struct[0][2] #args = @ee.evaluate([[:array, struct[0][2]]]) end end else block_type = line.match(BLOCK_MATCH) if block_type && block_type[0].strip != '' block_type = block_type[1] len = block_type.length block_type = block_type[0..-2] if block_type[-1] == '.' line = line[len..line.length] || '' else block_type = nil end end [block_type, args, line] end
@todo enhancement #1: allow same-line nesting of macros like `@macro(…) { @macro2 {` (and make sure to handle closing on same line like `} }`)
# File lib/eggshell/processor.rb, line 626 def parse_macro_start(line) macro = nil args = [] delim = nil # either macro is a plain '@macro' or it has parameters/opening brace if line.index(' ') || line.index('(') || line.index('{') # remove the end delimiter m = line.match(/(\{[\/\(\[a-z]*)\s*$/) if m line = line[0...line.rindex(m[1])] end line = line[1..-1] # since the macro statement is essentially a function call, parse the line as an expression to get components expr_struct = @ee.parse(line) fn = expr_struct.shift if fn.is_a?(Array) && (fn[0] == :func || fn[0] == :var) macro = fn[1] args = fn[2] # @@ee.evaluate([:array, fn[2]]) if m delim = m[1].reverse.gsub('{', '}').gsub('[', ']').gsub('(', ')') end end else macro = line[1..line.length] end [macro, args, delim] end
# File lib/eggshell/processor.rb, line 218 def preprocess(lines, line_count = 0) line_start = line_count line_buff = nil indent = 0 mode = nil in_html = false end_html = nil parse_tree = Eggshell::ParseTree.new """ algorithm for normalizing lines: - skip comments (process directive if present) - if line is continuation, set current line = last + current - if line ends in \ and is not blank otherwise, set new continuation and move to next line - if line ends in \ and is effectively blank, append '\n' - calculate indent level """ i = 0 begin while i < lines.length oline = lines[i] i += 1 line_count += 1 hdr = oline.lstrip[0..1] if hdr == COMMENT next end line = oline.chomp line_end = oline[line.length..-1] if line_buff line_buff += line line = line_buff line_buff = nil else line_start += 1 end _hard_return = false # if line ends in a single \, either insert hard return into current block (with \n) # or init line_buff to collect next line if line[-1] == '\\' if line[-2] != '\\' nline = line[0...-1] # check if line is effectively blank, but add leading whitespace back # to maintain tab processing if nline.strip == '' line = "#{nline}\n" line_end = '' _hard_return = true else line_buff = nline next end end end # detect tabs (must be consistent per-line) _ind = 0 tab_str = line[0] == TAB ? TAB : nil tab_str = line.index(TAB_SPACE) == 0 ? TAB_SPACE : nil if !tab_str indent_str = '' if tab_str _ind += 1 _len = tab_str.length _pos = _len while line.index(tab_str, _pos) _pos += _len _ind += 1 end line = line[_pos..-1] # trim indent chars based on block_handler_indent if indent > 0 _ind -= indent _ind = 0 if _ind < 0 end end line_norm = Line.new(line, tab_str, _ind, line_start, oline.chomp) line_start = line_count #$stderr.write ">> mode(#{parse_tree.mode}): #{line}\n" if parse_tree.mode == :raw stat = parse_tree.collect(line_norm) next if stat != BH::RETRY parse_tree.push_block elsif parse_tree.mode == :macro_raw if !parse_tree.macro_delim_match(line_norm, line_count) parse_tree.collect_macro_raw(line_norm) end next end # macro processing if line[0] == '@' macro, args, delim = parse_macro_start(line) #$stderr.write "-- macro: #{macro} (#{line})\n" mhandler = get_macro_handler(macro) parse_tree.new_macro(line_norm, line_count, macro, args, delim, mhandler ? mhandler.collection_type(macro) : nil) next elsif parse_tree.macro_delim_match(line_norm, line_count) next end if parse_tree.mode == :block stat = parse_tree.collect(line_norm) if stat == BH::RETRY parse_tree.push_block else next end end # blank line and not in block if line == '' parse_tree.push_block next end found = false @blocks.each do |handler| stat = handler.can_handle(line) next if stat == BH::RETRY parse_tree.new_block(handler, handler.current_type, line_norm, stat, line_count, self) found = true _trace "(#{handler.current_type}->#{handler}) #{line} -> #{stat}" break end if !found @blocks_map['p'].can_handle('p.') parse_tree.new_block(@blocks_map['p'], 'p', line_norm, BH::COLLECT, line_count, self) end end parse_tree.push_block # @todo check if macros left open rescue => ex _error "Exception approximately on line: #{line}" _error ex.message + "\t#{ex.backtrace.join("\n\t")}" #_error "vars = #{@vars.inspect}" end parse_tree end
# File lib/eggshell/processor.rb, line 499 def process(lines, line_count = 0, call_depth = 0) parse_tree = preprocess(lines, line_count) assemble(Eggshell::ParseTree.condense(self, parse_tree.tree), call_depth) end
# File lib/eggshell/processor.rb, line 68 def register_functions(handler, names = nil, ns = '') @ee.register_functions(handler, names, ns) end
# File lib/eggshell/processor.rb, line 42 def rem_block_handler(*names) _trace "rem_block_handler: #{names.inspect}" names.each do |name| handler = @blocks_map.delete(name) @blocks.delete(handler) end end
# File lib/eggshell/processor.rb, line 61 def rem_macro_handler(*names) _trace "rem_macro_handler: #{names.inspect}" names.each do |name| @macros.delete(name) end end
Sets the default output object. Must support {{<<}} and {{join(String)}}.
If {{out}} is a `Class`, must support empty initialization.
# File lib/eggshell/processor.rb, line 199 def set_out(out) @out = out end
# File lib/eggshell/processor.rb, line 691 def unescape(str) return self.class.unescape(str) end