eggshell 0.8.3 → 1.0.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.
- checksums.yaml +4 -4
- data/bin/eggshell +13 -0
- data/lib/eggshell/block-handler.rb +161 -23
- data/lib/eggshell/block.rb +3 -2
- data/lib/eggshell/bundles/basics-old.rb +850 -0
- data/lib/eggshell/bundles/basics.rb +650 -589
- data/lib/eggshell/bundles/loader.rb +6 -5
- data/lib/eggshell/expression-evaluator.rb +13 -3
- data/lib/eggshell/format-handler.rb +40 -0
- data/lib/eggshell/macro-handler.rb +16 -10
- data/lib/eggshell/parse-tree.rb +149 -0
- data/lib/eggshell/processor-context.rb +5 -18
- data/lib/eggshell/processor.rb +707 -0
- data/lib/eggshell.rb +42 -740
- metadata +10 -5
@@ -0,0 +1,707 @@
|
|
1
|
+
module Eggshell
|
2
|
+
class Processor
|
3
|
+
BLOCK_MATCH = /^([a-z0-9_-]+\.)/
|
4
|
+
BLOCK_MATCH_PARAMS = /^([a-z0-9_-]+)\(/
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@context = Eggshell::ProcessorContext.new
|
8
|
+
@vars = @context.vars
|
9
|
+
@funcs = @context.funcs
|
10
|
+
@macros = @context.macros
|
11
|
+
@blocks = @context.blocks
|
12
|
+
@blocks_map = @context.blocks_map
|
13
|
+
@block_params = @context.block_params
|
14
|
+
@expr_cache = @context.expr_cache
|
15
|
+
@fmt_handlers = @context.fmt_handlers
|
16
|
+
@ee = Eggshell::ExpressionEvaluator.new(@vars, @funcs)
|
17
|
+
|
18
|
+
@noop_macro = Eggshell::MacroHandler::Defaults::NoOpHandler.new
|
19
|
+
@noop_block = Eggshell::BlockHandler::Defaults::NoOpHandler.new
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_reader :context
|
23
|
+
|
24
|
+
def add_block_handler(handler, *names)
|
25
|
+
_trace "add_block_handler: #{names.inspect} -> #{handler.class}"
|
26
|
+
@blocks << handler
|
27
|
+
names.each do |name|
|
28
|
+
@blocks_map[name] = handler
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def rem_block_handler(*names)
|
33
|
+
_trace "rem_block_handler: #{names.inspect}"
|
34
|
+
names.each do |name|
|
35
|
+
handler = @blocks_map.delete(name)
|
36
|
+
@blocks.delete(handler)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def add_macro_handler(handler, *names)
|
41
|
+
_trace "add_macro_handler: #{names.inspect} -> #{handler.class}"
|
42
|
+
names.each do |name|
|
43
|
+
@macros[name] = handler
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def rem_macro_handler(*names)
|
48
|
+
_trace "rem_macro_handler: #{names.inspect}"
|
49
|
+
names.each do |name|
|
50
|
+
@macros.delete(name)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Registers a function for embedded expressions. Functions are grouped into namespaces,
|
55
|
+
# and a handler can be assigned to handle all function calls within that namespace, or
|
56
|
+
# a specific set of functions within the namespace. The root namespace is a blank string.
|
57
|
+
#
|
58
|
+
# @param String func_key In the form `ns` or `ns:func_name`. For functions in the
|
59
|
+
# root namespace, do `:func_name`.
|
60
|
+
# @param Object handler
|
61
|
+
# @param Array func_names If `func_key` only refers to a namespace but the handler
|
62
|
+
# needs to only handle a subset of functions, supply the list of function names here.
|
63
|
+
def register_functions(func_key, handler, func_names = nil)
|
64
|
+
@ee.register_functions(func_key, handler, func_names)
|
65
|
+
end
|
66
|
+
|
67
|
+
def _error(msg)
|
68
|
+
$stderr.write("[ERROR] #{msg}\n")
|
69
|
+
end
|
70
|
+
|
71
|
+
def _warn(msg)
|
72
|
+
$stderr.write("[WARN] #{msg}\n")
|
73
|
+
end
|
74
|
+
|
75
|
+
def _info(msg)
|
76
|
+
return if @vars['log.level'] < 1
|
77
|
+
$stderr.write("[INFO] #{msg}\n")
|
78
|
+
end
|
79
|
+
|
80
|
+
def _debug(msg)
|
81
|
+
return if @vars['log.level'] < 2
|
82
|
+
$stderr.write("[DEBUG] #{msg}\n")
|
83
|
+
end
|
84
|
+
|
85
|
+
def _trace(msg)
|
86
|
+
return if @vars['log.level'] < 3
|
87
|
+
$stderr.write("[TRACE] #{msg}\n")
|
88
|
+
end
|
89
|
+
|
90
|
+
attr_reader :vars
|
91
|
+
|
92
|
+
def expr_eval(struct)
|
93
|
+
return Eggshell::ExpressionEvaluator.expr_eval(struct, @vars, @funcs)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Expands expressions (`\${}`) and macro calls (`\@@macro\@@`).
|
97
|
+
# @todo deprecate @@macro@@?
|
98
|
+
def expand_expr(expr)
|
99
|
+
# replace dynamic placeholders
|
100
|
+
# @todo expand to actual expressions
|
101
|
+
buff = []
|
102
|
+
esc = false
|
103
|
+
exp = false
|
104
|
+
mac = false
|
105
|
+
|
106
|
+
toks = expr.split(/(\\|\$\{|\}|@@|"|')/)
|
107
|
+
i = 0
|
108
|
+
|
109
|
+
plain_str = ''
|
110
|
+
expr_str = ''
|
111
|
+
quote = nil
|
112
|
+
expr_delim = nil
|
113
|
+
|
114
|
+
while i < toks.length
|
115
|
+
tok = toks[i]
|
116
|
+
i += 1
|
117
|
+
next if tok == ''
|
118
|
+
|
119
|
+
if esc
|
120
|
+
plain_str += '\\' + tok
|
121
|
+
esc = false
|
122
|
+
next
|
123
|
+
end
|
124
|
+
|
125
|
+
if exp
|
126
|
+
if quote
|
127
|
+
expr_str += tok
|
128
|
+
if tok == quote
|
129
|
+
quote = nil
|
130
|
+
end
|
131
|
+
elsif tok == '"' || tok == "'"
|
132
|
+
expr_str += tok
|
133
|
+
quote = tok
|
134
|
+
elsif tok == expr_delim
|
135
|
+
struct = @expr_cache[expr_str]
|
136
|
+
|
137
|
+
if !struct
|
138
|
+
struct = Eggshell::ExpressionEvaluator.struct(expr_str)
|
139
|
+
@expr_cache[expr_str] = struct
|
140
|
+
end
|
141
|
+
|
142
|
+
if !mac
|
143
|
+
buff << expr_eval(struct)
|
144
|
+
else
|
145
|
+
args = struct[0]
|
146
|
+
macro = args[1]
|
147
|
+
args = args[2] || []
|
148
|
+
macro_handler = @macros[macro]
|
149
|
+
if macro_handler
|
150
|
+
macro_handler.process(buff, macro, args, nil, -1)
|
151
|
+
else
|
152
|
+
_warn("macro (inline) not found: #{macro}")
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
exp = false
|
157
|
+
mac = false
|
158
|
+
expr_delim = nil
|
159
|
+
expr_str = ''
|
160
|
+
else
|
161
|
+
expr_str += tok
|
162
|
+
end
|
163
|
+
# only unescape if not in expression, since expression needs to be given as-is
|
164
|
+
elsif tok == '\\'
|
165
|
+
esc = true
|
166
|
+
next
|
167
|
+
elsif tok == '${' || tok == '@@'
|
168
|
+
if plain_str != ''
|
169
|
+
buff << plain_str
|
170
|
+
plain_str = ''
|
171
|
+
end
|
172
|
+
exp = true
|
173
|
+
expr_delim = '}'
|
174
|
+
if tok == '@@'
|
175
|
+
mac = true
|
176
|
+
expr_delim = tok
|
177
|
+
end
|
178
|
+
else
|
179
|
+
plain_str += tok
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# if exp -- throw exception?
|
184
|
+
buff << plain_str if plain_str != ''
|
185
|
+
return buff.join('')
|
186
|
+
end
|
187
|
+
|
188
|
+
TAB = "\t"
|
189
|
+
TAB_SPACE = ' '
|
190
|
+
|
191
|
+
# @param Boolean is_default If true, associates these parameters with the
|
192
|
+
# `block_type` used in `get_block_param()` or explicitly in third parameter.
|
193
|
+
# @param String block_type
|
194
|
+
def set_block_params(params, is_default = false, block_type = nil)
|
195
|
+
if block_type && is_default
|
196
|
+
@block_params[block_type] = params
|
197
|
+
else
|
198
|
+
@block_params[:pending] = params
|
199
|
+
@block_param_default = is_default
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
# Gets the block parameters for a block type, and merges default values if available.
|
204
|
+
def get_block_params(block_type)
|
205
|
+
bp = @block_params.delete(:pending)
|
206
|
+
if @block_params_default
|
207
|
+
if block_type && bp
|
208
|
+
@block_params[block_type] = bp if bp
|
209
|
+
end
|
210
|
+
@block_params_default = false
|
211
|
+
bp = {} if !bp
|
212
|
+
else
|
213
|
+
bp = {} if !bp
|
214
|
+
default = @block_params[block_type]
|
215
|
+
if default
|
216
|
+
default.each do |key,val|
|
217
|
+
if !bp.has_key?(key) && val
|
218
|
+
bp[key] = val.clone
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
return bp
|
224
|
+
end
|
225
|
+
|
226
|
+
# Sets the default output object. Must support {{<<}} and {{join(String)}}.
|
227
|
+
#
|
228
|
+
# If {{out}} is a `Class`, must support empty initialization.
|
229
|
+
def set_out(out)
|
230
|
+
@out = out
|
231
|
+
end
|
232
|
+
|
233
|
+
def get_out
|
234
|
+
if !@out
|
235
|
+
[]
|
236
|
+
elsif @out.is_a?(Class)
|
237
|
+
@out.new
|
238
|
+
else
|
239
|
+
@out
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
BH = Eggshell::BlockHandler
|
244
|
+
|
245
|
+
def preprocess(lines, line_count = 0)
|
246
|
+
line_start = line_count
|
247
|
+
line_buff = nil
|
248
|
+
indent = 0
|
249
|
+
mode = nil
|
250
|
+
|
251
|
+
in_html = false
|
252
|
+
end_html = nil
|
253
|
+
|
254
|
+
parse_tree = Eggshell::ParseTree.new
|
255
|
+
|
256
|
+
"""
|
257
|
+
algorithm for normalizing lines:
|
258
|
+
|
259
|
+
- skip comments (process directive if present)
|
260
|
+
- if line is continuation, set current line = last + current
|
261
|
+
- if line ends in \ and is not blank otherwise, set new continuation and move to next line
|
262
|
+
- if line ends in \ and is effectively blank, append '\n'
|
263
|
+
- calculate indent level
|
264
|
+
"""
|
265
|
+
i = 0
|
266
|
+
begin
|
267
|
+
while i < lines.length
|
268
|
+
oline = lines[i]
|
269
|
+
i += 1
|
270
|
+
|
271
|
+
line_count += 1
|
272
|
+
|
273
|
+
hdr = oline.lstrip[0..1]
|
274
|
+
if hdr == '#!'
|
275
|
+
next
|
276
|
+
end
|
277
|
+
|
278
|
+
line = oline.chomp
|
279
|
+
line_end = oline[line.length..-1]
|
280
|
+
if line_buff
|
281
|
+
line_buff += line
|
282
|
+
line = line_buff
|
283
|
+
line_buff = nil
|
284
|
+
else
|
285
|
+
line_start += 1
|
286
|
+
end
|
287
|
+
|
288
|
+
_hard_return = false
|
289
|
+
|
290
|
+
# if line ends in a single \, either insert hard return into current block (with \n)
|
291
|
+
# or init line_buff to collect next line
|
292
|
+
if line[-1] == '\\'
|
293
|
+
if line[-2] != '\\'
|
294
|
+
nline = line[0...-1]
|
295
|
+
# check if line is effectively blank, but add leading whitespace back
|
296
|
+
# to maintain tab processing
|
297
|
+
if nline.strip == ''
|
298
|
+
line = "#{nline}\n"
|
299
|
+
line_end = ''
|
300
|
+
_hard_return = true
|
301
|
+
else
|
302
|
+
line_buff = nline
|
303
|
+
next
|
304
|
+
end
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
# detect tabs (must be consistent per-line)
|
309
|
+
_ind = 0
|
310
|
+
tab_str = line[0] == TAB ? TAB : nil
|
311
|
+
tab_str = line.index(TAB_SPACE) == 0 ? TAB_SPACE : nil if !tab_str
|
312
|
+
indent_str = ''
|
313
|
+
if tab_str
|
314
|
+
_ind += 1
|
315
|
+
_len = tab_str.length
|
316
|
+
_pos = _len
|
317
|
+
while line.index(tab_str, _pos)
|
318
|
+
_pos += _len
|
319
|
+
_ind += 1
|
320
|
+
end
|
321
|
+
line = line[_pos..-1]
|
322
|
+
|
323
|
+
# trim indent chars based on block_handler_indent
|
324
|
+
if indent > 0
|
325
|
+
_ind -= indent
|
326
|
+
_ind = 0 if _ind < 0
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
line_norm = Line.new(line, tab_str, _ind, line_start, oline.chomp)
|
331
|
+
line_start = line_count
|
332
|
+
|
333
|
+
if parse_tree.mode == :raw
|
334
|
+
stat = parse_tree.collect(line_norm)
|
335
|
+
next if stat != BH::RETRY
|
336
|
+
parse_tree.push_block
|
337
|
+
end
|
338
|
+
|
339
|
+
# macro processing
|
340
|
+
if line[0] == '@'
|
341
|
+
parse_tree.new_macro(line_norm, line_count)
|
342
|
+
next
|
343
|
+
elsif parse_tree.macro_delim_match(line_norm, line_count)
|
344
|
+
next
|
345
|
+
end
|
346
|
+
|
347
|
+
if parse_tree.mode == :block
|
348
|
+
stat = parse_tree.collect(line_norm)
|
349
|
+
if stat == BH::RETRY
|
350
|
+
parse_tree.push_block
|
351
|
+
else
|
352
|
+
next
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
# blank line and not in block
|
357
|
+
if line == ''
|
358
|
+
parse_tree.push_block
|
359
|
+
next
|
360
|
+
end
|
361
|
+
|
362
|
+
found = false
|
363
|
+
@blocks.each do |handler|
|
364
|
+
stat = handler.can_handle(line)
|
365
|
+
next if stat == BH::RETRY
|
366
|
+
|
367
|
+
parse_tree.new_block(handler, handler.current_type, line_norm, stat, line_count)
|
368
|
+
found = true
|
369
|
+
_trace "(#{handler.current_type}->#{handler}) #{line} -> #{stat}"
|
370
|
+
break
|
371
|
+
end
|
372
|
+
|
373
|
+
if !found
|
374
|
+
@blocks_map['p'].can_handle('p.')
|
375
|
+
parse_tree.new_block(@blocks_map['p'], 'p', line_norm, BH::COLLECT, line_count)
|
376
|
+
end
|
377
|
+
end
|
378
|
+
parse_tree.push_block
|
379
|
+
# @todo check if macros left open
|
380
|
+
rescue => ex
|
381
|
+
_error "Exception approximately on line: #{line}"
|
382
|
+
_error ex.message + "\t#{ex.backtrace.join("\n\t")}"
|
383
|
+
#_error "vars = #{@vars.inspect}"
|
384
|
+
end
|
385
|
+
|
386
|
+
parse_tree
|
387
|
+
end
|
388
|
+
|
389
|
+
# This string in a block indicates that a piped macro's output should be inserted at this location
|
390
|
+
# rather than immediately after last line. For now, this is only checked for on the last line.
|
391
|
+
#
|
392
|
+
# Multiple inline pipes can be specified on this line, with each pipe corresponding to each macro
|
393
|
+
# chained to the block. Any unfilled pipe will be replaced with a blank string.
|
394
|
+
#
|
395
|
+
# To escape the pipe, use a backslash anywhere [*AFTER*] the initial dash (e.g. `-\\>*<-`).
|
396
|
+
PIPE_INLINE = '->*<-'
|
397
|
+
|
398
|
+
# Goes through each item in parse tree, collecting output in the following manner:
|
399
|
+
#
|
400
|
+
# # {{String}}s and {{Line}}s are outputted as-is
|
401
|
+
# # macros and blocks with matching handlers get {{process}} called
|
402
|
+
#
|
403
|
+
# All output is joined with `\\n` by default.
|
404
|
+
#
|
405
|
+
# The output object and join string can be overridden through the {{opts}} parameter
|
406
|
+
# keys {{:out}} and {{:joiner}}.
|
407
|
+
#3
|
408
|
+
# @param Eggshell::ParseTree,Array parse_tree Parsed document.
|
409
|
+
# @param Integer call_depth
|
410
|
+
# @param Hash opts
|
411
|
+
def assemble(parse_tree, call_depth = 0, opts = {})
|
412
|
+
opts = {} if !opts.is_a?(Hash)
|
413
|
+
out = opts[:out] || get_out
|
414
|
+
joiner = opts[:join] || "\n"
|
415
|
+
|
416
|
+
parse_tree = parse_tree.tree if parse_tree.is_a?(Eggshell::ParseTree)
|
417
|
+
raise Exception.new("input not an array or ParseTree (depth=#{call_depth}") if !parse_tree.is_a?(Array)
|
418
|
+
# @todo defer process to next unit so macro can inject lines back into previous block
|
419
|
+
|
420
|
+
last_type = nil
|
421
|
+
last_line = 0
|
422
|
+
deferred = nil
|
423
|
+
|
424
|
+
parse_tree.each do |unit|
|
425
|
+
if unit.is_a?(String)
|
426
|
+
out << unit
|
427
|
+
last_line += 1
|
428
|
+
elsif unit.is_a?(Eggshell::Line)
|
429
|
+
out << unit.to_s
|
430
|
+
last_line = unit.line_nameum
|
431
|
+
elsif unit.is_a?(Array)
|
432
|
+
handler = unit[0] == :block ? @blocks_map[unit[1]] : @macros[unit[1]]
|
433
|
+
name = unit[1]
|
434
|
+
|
435
|
+
if !handler
|
436
|
+
_warn "handler not found: #{unit[0]} -> #{unit[1]}"
|
437
|
+
next
|
438
|
+
end
|
439
|
+
|
440
|
+
args_o = unit[2] || []
|
441
|
+
args = []
|
442
|
+
args_o.each do |arg|
|
443
|
+
args << expr_eval(arg)
|
444
|
+
end
|
445
|
+
|
446
|
+
lines = unit[ParseTree::IDX_LINES]
|
447
|
+
lines_start = unit[ParseTree::IDX_LINES_START]
|
448
|
+
lines_end = unit[ParseTree::IDX_LINES_END]
|
449
|
+
|
450
|
+
_handler, _name, _args, _lines = deferred
|
451
|
+
|
452
|
+
if unit[0] == :block
|
453
|
+
if deferred
|
454
|
+
# two cases:
|
455
|
+
# 1. this block is immediately tied to block-macro chain and is continuation of same type of block
|
456
|
+
# 2. part of block-macro chain but not same type, or immediately follows another block
|
457
|
+
|
458
|
+
if last_type == :macro && (lines_end - last_line == 1) && _name == name
|
459
|
+
lines.each do |line|
|
460
|
+
_lines << line
|
461
|
+
end
|
462
|
+
else
|
463
|
+
_handler.process(_name, _args, _lines, out, call_depth)
|
464
|
+
deferred = [handler, name, args, lines.clone]
|
465
|
+
end
|
466
|
+
else
|
467
|
+
deferred = [handler, name, args, lines.clone]
|
468
|
+
end
|
469
|
+
|
470
|
+
last_line = lines_end
|
471
|
+
else
|
472
|
+
# macro immediately after a block, so assume that output gets piped into last lines
|
473
|
+
# of closest block
|
474
|
+
if deferred && lines_start - last_line == 1
|
475
|
+
_last = _lines[-1]
|
476
|
+
pinline = false
|
477
|
+
pipe = _lines
|
478
|
+
if _last.to_s.index(PIPE_INLINE)
|
479
|
+
pipe = []
|
480
|
+
pinline = true
|
481
|
+
end
|
482
|
+
|
483
|
+
handler.process(name, args, lines, pipe, call_depth)
|
484
|
+
|
485
|
+
# inline pipe; join output with literal \n to avoid processing lines in block process
|
486
|
+
if pinline
|
487
|
+
if _last.is_a?(Eggshell::Line)
|
488
|
+
_lines[-1] = _last.replace(_last.line.sub(PIPE_INLINE, pipe.join('\n')))
|
489
|
+
else
|
490
|
+
_lines[-1] = _last.sub(PIPE_INLINE, pipe.join('\n'))
|
491
|
+
end
|
492
|
+
end
|
493
|
+
else
|
494
|
+
if deferred
|
495
|
+
_handler.process(_name, _args, _lines, out, call_depth)
|
496
|
+
deferred = nil
|
497
|
+
end
|
498
|
+
handler.process(name, args, lines, out, call_depth)
|
499
|
+
end
|
500
|
+
last_line = lines_end
|
501
|
+
end
|
502
|
+
|
503
|
+
last_type = unit[0]
|
504
|
+
elsif unit
|
505
|
+
_warn "not sure how to handle #{unit.class}"
|
506
|
+
_debug unit.inspect
|
507
|
+
end
|
508
|
+
end
|
509
|
+
|
510
|
+
if deferred
|
511
|
+
_handler, _name, _args, _lines = deferred
|
512
|
+
_handler.process(_name, _args, _lines, out, call_depth)
|
513
|
+
deferred = nil
|
514
|
+
end
|
515
|
+
out.join(joiner)
|
516
|
+
end
|
517
|
+
|
518
|
+
def process(lines, line_count = 0, call_depth = 0)
|
519
|
+
parse_tree = preprocess(lines, line_count)
|
520
|
+
assemble(parse_tree.tree, call_depth)
|
521
|
+
end
|
522
|
+
|
523
|
+
# Register inline format handlers with opening and closing tags.
|
524
|
+
# Typically, tags can be arbitrarily nested. However, nesting can be
|
525
|
+
# shut off completely or selectively by specifying 0 or more tags
|
526
|
+
# separated by a space (empty string is completely disabled).
|
527
|
+
#
|
528
|
+
# @param Array tags Each entry should be a 2- or 3-element array in the
|
529
|
+
# following form: {{[open, close[, non_nest]]}}
|
530
|
+
def add_format_handler(handler, tags)
|
531
|
+
return if !tags.is_a?(Array)
|
532
|
+
|
533
|
+
tags.each do |entry|
|
534
|
+
open, close, no_nest = entry
|
535
|
+
no_nest = '' if no_nest.is_a?(TrueClass)
|
536
|
+
@fmt_handlers[open] = [handler, close, no_nest]
|
537
|
+
_trace "add_format_handler: #{open} #{close} (non-nested: #{no_nest.inspect})"
|
538
|
+
end
|
539
|
+
|
540
|
+
# regenerate splitting pattern going from longest to shortest
|
541
|
+
openers = @fmt_handlers.keys.sort do |a, b|
|
542
|
+
b.length <=> a.length
|
543
|
+
end
|
544
|
+
|
545
|
+
regex = ''
|
546
|
+
openers.each do |op|
|
547
|
+
regex = "#{regex}|#{Regexp.quote(op)}|#{Regexp.quote(@fmt_handlers[op][1])}"
|
548
|
+
end
|
549
|
+
|
550
|
+
@fmt_regex = /(\\|'|"#{regex})/
|
551
|
+
end
|
552
|
+
|
553
|
+
# Expands inline formatting with {{Eggshell::FormatHandler}}s.
|
554
|
+
def expand_formatting(str)
|
555
|
+
toks = str.gsub(PIPE_INLINE, '').split(@fmt_regex)
|
556
|
+
toks.delete('')
|
557
|
+
|
558
|
+
buff = ['']
|
559
|
+
quote = nil
|
560
|
+
opened = []
|
561
|
+
closing = []
|
562
|
+
non_nesting = []
|
563
|
+
|
564
|
+
i = 0
|
565
|
+
while i < toks.length
|
566
|
+
tok = toks[i]
|
567
|
+
i += 1
|
568
|
+
if tok == '\\'
|
569
|
+
# preserve escape char otherwise we lose things like \n or \t
|
570
|
+
buff[-1] += tok + toks[i]
|
571
|
+
i += 1
|
572
|
+
elsif quote
|
573
|
+
quote = nil if tok == quote
|
574
|
+
buff[-1] += tok
|
575
|
+
elsif tok == '"' || tok == "'"
|
576
|
+
quote = tok if opened[-1]
|
577
|
+
buff[-1] += tok
|
578
|
+
elsif @fmt_handlers[tok] && (!non_nesting[-1] || non_nesting.index(tok))
|
579
|
+
handler, closer, non_nest = @fmt_handlers[tok]
|
580
|
+
opened << tok
|
581
|
+
closing << closer
|
582
|
+
non_nesting << non_nest
|
583
|
+
buff << ''
|
584
|
+
elsif tok == closing[-1]
|
585
|
+
opener = opened.pop
|
586
|
+
handler = @fmt_handlers[opener][0]
|
587
|
+
closing.pop
|
588
|
+
non_nesting.pop
|
589
|
+
|
590
|
+
# @todo insert placeholder and swap out at end? might be a prob if value has to be escaped
|
591
|
+
bstr = buff.pop
|
592
|
+
buff[-1] += handler.format(opener, bstr)
|
593
|
+
else
|
594
|
+
buff[-1] += tok
|
595
|
+
end
|
596
|
+
end
|
597
|
+
|
598
|
+
opened.each do |op|
|
599
|
+
bstr = buff.pop
|
600
|
+
buff[-1] += op + bstr
|
601
|
+
_warn "expand_formatting: unclosed #{op}, not doing anything: #{bstr}"
|
602
|
+
end
|
603
|
+
|
604
|
+
buff.join('')
|
605
|
+
end
|
606
|
+
|
607
|
+
def self.parse_block_start(line)
|
608
|
+
block_type = nil
|
609
|
+
args = []
|
610
|
+
|
611
|
+
bt = line.match(BLOCK_MATCH_PARAMS)
|
612
|
+
if bt
|
613
|
+
idx0 = bt[0].length
|
614
|
+
idx1 = line.index(').', idx0)
|
615
|
+
if idx1
|
616
|
+
block_type = line[0..idx0-2]
|
617
|
+
params = line[0...idx1+1].strip
|
618
|
+
line = line[idx1+2..line.length] || ''
|
619
|
+
if params != ''
|
620
|
+
struct = ExpressionEvaluator.struct(params)
|
621
|
+
args = struct[0][2]
|
622
|
+
end
|
623
|
+
end
|
624
|
+
else
|
625
|
+
block_type = line.match(BLOCK_MATCH)
|
626
|
+
if block_type && block_type[0].strip != ''
|
627
|
+
block_type = block_type[1]
|
628
|
+
len = block_type.length
|
629
|
+
block_type = block_type[0..-2] if block_type[-1] == '.'
|
630
|
+
line = line[len..line.length] || ''
|
631
|
+
else
|
632
|
+
block_type = nil
|
633
|
+
end
|
634
|
+
end
|
635
|
+
|
636
|
+
[block_type, args, line]
|
637
|
+
end
|
638
|
+
|
639
|
+
def self.parse_macro_start(line)
|
640
|
+
macro = nil
|
641
|
+
args = []
|
642
|
+
delim = nil
|
643
|
+
|
644
|
+
# either macro is a plain '@macro' or it has parameters/opening brace
|
645
|
+
if line.index(' ') || line.index('(') || line.index('{')
|
646
|
+
# since the macro statement is essentially a function call, parse the line as an expression to get components
|
647
|
+
expr_struct = ExpressionEvaluator.struct(line)
|
648
|
+
fn = expr_struct.shift
|
649
|
+
if fn.is_a?(Array) && (fn[0] == :fn || fn[0] == :var)
|
650
|
+
macro = fn[1][1..fn[1].length]
|
651
|
+
args = fn[2]
|
652
|
+
if expr_struct[-1].is_a?(Array) && expr_struct[-1][0] == :brace_op
|
653
|
+
delim = expr_struct[-1][1]
|
654
|
+
end
|
655
|
+
end
|
656
|
+
else
|
657
|
+
macro = line[1..line.length]
|
658
|
+
end
|
659
|
+
|
660
|
+
[macro, args, delim]
|
661
|
+
end
|
662
|
+
|
663
|
+
BACKSLASH_REGEX = /\\(u[0-9a-f]{4}|u\{[^}]+\}|.)/i
|
664
|
+
BACKSLASH_UNESCAPE_MAP = {
|
665
|
+
'f' => "\f",
|
666
|
+
'n' => "\n",
|
667
|
+
'r' => "\r",
|
668
|
+
't' => "\t",
|
669
|
+
'v' => "\v"
|
670
|
+
}.freeze
|
671
|
+
|
672
|
+
# Unescapes backslashes and Unicode characters.
|
673
|
+
#
|
674
|
+
# If a match is made against {{BACKSLASH_UNESCAPE_MAP}} that character will
|
675
|
+
# be used, otherwise, the literal is used.
|
676
|
+
#
|
677
|
+
# Unicode sequences are standard Ruby-like syntax: {{\uabcd}} or {{\u{seq1 seq2 ...}}}.
|
678
|
+
def self.unescape(str)
|
679
|
+
str = str.gsub(BACKSLASH_REGEX) do |match|
|
680
|
+
if match.length == 2
|
681
|
+
c = match[1]
|
682
|
+
BACKSLASH_UNESCAPE_MAP[c] || c
|
683
|
+
else
|
684
|
+
if match[2] == '{'
|
685
|
+
parts = match[3..-1].split(' ')
|
686
|
+
buff = ''
|
687
|
+
parts.each do |part|
|
688
|
+
buff += [part.to_i(16)].pack('U')
|
689
|
+
end
|
690
|
+
buff
|
691
|
+
else
|
692
|
+
[match[2..-1].to_i(16)].pack('U')
|
693
|
+
end
|
694
|
+
end
|
695
|
+
end
|
696
|
+
end
|
697
|
+
|
698
|
+
def unescape(str)
|
699
|
+
return self.class.unescape(str)
|
700
|
+
end
|
701
|
+
|
702
|
+
# Calls inline formatting, expression extrapolator, and backslash unescape.
|
703
|
+
def expand_all(str)
|
704
|
+
unescape(expand_expr(expand_formatting(str)))
|
705
|
+
end
|
706
|
+
end
|
707
|
+
end
|