1 | Node = require('./nodes/node')
|
2 | Text = require('./nodes/text')
|
3 | Haml = require('./nodes/haml')
|
4 | Code = require('./nodes/code')
|
5 | Comment = require('./nodes/comment')
|
6 | Filter = require('./nodes/filter')
|
7 |
|
8 | {whitespace} = require('./util/text')
|
9 | {indent} = require('./util/text')
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 | module.exports = class HamlCoffee
|
16 |
|
17 |
|
18 | @VERSION: '1.9.1'
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 | constructor: (@options = {}) ->
|
41 | @options.placement ?= 'global'
|
42 | @options.dependencies ?= { hc: 'hamlcoffee' }
|
43 | @options.escapeHtml ?= true
|
44 | @options.escapeAttributes ?= true
|
45 | @options.cleanValue ?= true
|
46 | @options.uglify ?= false
|
47 | @options.basename ?= false
|
48 | @options.extendScope ?= false
|
49 | @options.format ?= 'html5'
|
50 | @options.preserveTags ?= 'pre,textarea'
|
51 | @options.selfCloseTags ?= 'meta,img,link,br,hr,input,area,param,col,base'
|
52 |
|
53 |
|
54 |
|
55 |
|
56 |
|
57 |
|
58 | indentChanged: ->
|
59 | @currentIndent != @previousIndent
|
60 |
|
61 |
|
62 |
|
63 |
|
64 |
|
65 | isIndent: ->
|
66 | @currentIndent > @previousIndent
|
67 |
|
68 |
|
69 |
|
70 | updateTabSize: ->
|
71 | @tabSize = @currentIndent - @previousIndent if @tabSize == 0
|
72 |
|
73 |
|
74 |
|
75 | updateBlockLevel: ->
|
76 | @currentBlockLevel = @currentIndent / @tabSize
|
77 |
|
78 |
|
79 | if @currentBlockLevel - Math.floor(@currentBlockLevel) > 0
|
80 | throw("Indentation error in line #{ @lineNumber }")
|
81 |
|
82 |
|
83 | if (@currentIndent - @previousIndent) / @tabSize > 1
|
84 |
|
85 | unless @node.isCommented()
|
86 | throw("Block level too deep in line #{ @lineNumber }")
|
87 |
|
88 |
|
89 | @delta = @previousBlockLevel - @currentBlockLevel
|
90 |
|
91 |
|
92 |
|
93 |
|
94 |
|
95 | updateCodeBlockLevel: (node) ->
|
96 | if node instanceof Code
|
97 | @currentCodeBlockLevel = node.codeBlockLevel + 1
|
98 | else
|
99 | @currentCodeBlockLevel = node.codeBlockLevel
|
100 |
|
101 |
|
102 |
|
103 |
|
104 | updateParent: ->
|
105 | if @isIndent()
|
106 | @pushParent()
|
107 | else
|
108 | @popParent()
|
109 |
|
110 |
|
111 |
|
112 |
|
113 |
|
114 | pushParent: ->
|
115 | @stack.push @parentNode
|
116 | @parentNode = @node
|
117 |
|
118 |
|
119 |
|
120 |
|
121 | popParent: ->
|
122 | for i in [0..@delta-1]
|
123 | @parentNode = @stack.pop()
|
124 |
|
125 |
|
126 |
|
127 |
|
128 |
|
129 |
|
130 | getNodeOptions: (override = {})->
|
131 | {
|
132 | parentNode : override.parentNode || @parentNode
|
133 | blockLevel : override.blockLevel || @currentBlockLevel
|
134 | codeBlockLevel : override.codeBlockLevel || @currentCodeBlockLevel
|
135 | escapeHtml : override.escapeHtml || @options.escapeHtml
|
136 | escapeAttributes : override.escapeAttributes || @options.escapeAttributes
|
137 | cleanValue : override.cleanValue || @options.cleanValue
|
138 | format : override.format || @options.format
|
139 | preserveTags : override.preserveTags || @options.preserveTags
|
140 | selfCloseTags : override.selfCloseTags || @options.selfCloseTags
|
141 | uglify : override.uglify || @options.uglify
|
142 | }
|
143 |
|
144 |
|
145 |
|
146 |
|
147 |
|
148 |
|
149 |
|
150 |
|
151 |
|
152 |
|
153 | nodeFactory: (expression = '') ->
|
154 |
|
155 | options = @getNodeOptions()
|
156 |
|
157 |
|
158 | if expression.match(/^:(escaped|preserve|css|javascript|plain|cdata|coffeescript)/)
|
159 | node = new Filter(expression, options)
|
160 |
|
161 |
|
162 | else if expression.match(/^(\/|-#)(.*)/)
|
163 | node = new Comment(expression, options)
|
164 |
|
165 |
|
166 | else if expression.match(/^(-#|-|=|!=|\&=|~)\s*(.*)/)
|
167 | node = new Code(expression, options)
|
168 |
|
169 |
|
170 | else if expression.match(/^(%|#[^{]|\.|\!)(.*)/)
|
171 | node = new Haml(expression, options)
|
172 |
|
173 |
|
174 | else
|
175 | node = new Text(expression, options)
|
176 |
|
177 | options.parentNode?.addChild(node)
|
178 |
|
179 | node
|
180 |
|
181 |
|
182 |
|
183 |
|
184 |
|
185 |
|
186 |
|
187 |
|
188 |
|
189 |
|
190 |
|
191 |
|
192 | parse: (source = '') ->
|
193 |
|
194 | @lineNumber = @previousIndent = @tabSize = @currentBlockLevel = @previousBlockLevel = 0
|
195 | @currentCodeBlockLevel = @previousCodeBlockLevel = 0
|
196 |
|
197 |
|
198 | @node = null
|
199 | @stack = []
|
200 | @root = @parentNode = new Node('', @getNodeOptions())
|
201 |
|
202 |
|
203 | lines = source.split("\n")
|
204 |
|
205 |
|
206 | while (line = lines.shift()) isnt undefined
|
207 |
|
208 |
|
209 | if (@node instanceof Filter) and not @exitFilter
|
210 |
|
211 |
|
212 | if /^(\s)*$/.test(line)
|
213 | @node.addChild(new Text('', @getNodeOptions({ parentNode: @node })))
|
214 |
|
215 |
|
216 | else
|
217 | result = line.match /^(\s*)(.*)/
|
218 | ws = result[1]
|
219 | expression = result[2]
|
220 |
|
221 |
|
222 | if @node.blockLevel >= (ws.length / 2)
|
223 | @exitFilter = true
|
224 | lines.unshift line
|
225 | continue
|
226 |
|
227 |
|
228 | text = line.match ///^\s{#{ (@node.blockLevel * 2) + 2 }}(.*)///
|
229 | @node.addChild(new Text(text[1], @getNodeOptions({ parentNode: @node }))) if text
|
230 |
|
231 |
|
232 | else
|
233 |
|
234 |
|
235 | @exitFilter = false
|
236 |
|
237 |
|
238 | result = line.match /^(\s*)(.*)/
|
239 | ws = result[1]
|
240 | expression = result[2]
|
241 |
|
242 |
|
243 | continue if /^\s*$/.test(line)
|
244 |
|
245 |
|
246 | while /^[%.#].*[{(]/.test(expression) and not /^(\s*)[-=&!~.%#</]/.test(lines[0]) and /([-\w]+[\w:-]*\w?)\s*=|('\w+[\w:-]*\w?')\s*=|("\w+[\w:-]*\w?")\s*=|(\w+[\w:-]*\w?):|('[-\w]+[\w:-]*\w?'):|("[-\w]+[\w:-]*\w?"):|:(\w+[\w:-]*\w?)\s*=>|:?'([-\w]+[\w:-]*\w?)'\s*=>|:?"([-\w]+[\w:-]*\w?)"\s*=>/.test(lines[0])
|
247 |
|
248 | attributes = lines.shift()
|
249 | expression = expression.replace(/(\s)+\|\s*$/, '')
|
250 | expression += ' ' + attributes.match(/^\s*(.*?)(\s+\|\s*)?$/)[1]
|
251 | @lineNumber++
|
252 |
|
253 |
|
254 | if expression.match(/(\s)+\|\s*$/)
|
255 | expression = expression.replace(/(\s)+\|\s*$/, ' ')
|
256 |
|
257 | while lines[0]?.match(/(\s)+\|$/)
|
258 | expression += lines.shift().match(/^(\s*)(.*)/)[2].replace(/(\s)+\|\s*$/, '')
|
259 | @lineNumber++
|
260 |
|
261 | @currentIndent = ws.length
|
262 |
|
263 |
|
264 | if @indentChanged()
|
265 | @updateTabSize()
|
266 | @updateBlockLevel()
|
267 | @updateParent()
|
268 | @updateCodeBlockLevel(@parentNode)
|
269 |
|
270 |
|
271 | @node = @nodeFactory(expression)
|
272 |
|
273 |
|
274 | @previousBlockLevel = @currentBlockLevel
|
275 | @previousIndent = @currentIndent
|
276 |
|
277 | @lineNumber++
|
278 |
|
279 | @evaluate(@root)
|
280 |
|
281 |
|
282 |
|
283 |
|
284 |
|
285 | evaluate: (node) ->
|
286 | @evaluate(child) for child in node.children
|
287 | node.evaluate()
|
288 |
|
289 |
|
290 |
|
291 |
|
292 |
|
293 |
|
294 | render: (templateName, namespace = 'window.HAML') ->
|
295 | switch @options.placement
|
296 | when 'amd'
|
297 | @renderAmd()
|
298 | else
|
299 | @renderGlobal templateName, namespace
|
300 |
|
301 |
|
302 |
|
303 |
|
304 |
|
305 |
|
306 |
|
307 |
|
308 |
|
309 |
|
310 | renderAmd: ->
|
311 | if /^hamlcoffee/.test @options.dependencies['hc']
|
312 | @options.customHtmlEscape = 'hc.escape'
|
313 | @options.customCleanValue = 'hc.cleanValue'
|
314 | @options.customPreserve = 'hc.preserve'
|
315 | @options.customFindAndPreserve = 'hc.findAndPreserve'
|
316 | @options.customSurround = 'hc.surround'
|
317 | @options.customSucceed = 'hc.succeed'
|
318 | @options.customPrecede = 'hc.precede'
|
319 | @options.customReference = 'hc.reference'
|
320 |
|
321 | modules = []
|
322 | params = []
|
323 |
|
324 | for param, module of @options.dependencies
|
325 | modules.push module
|
326 | params.push param
|
327 |
|
328 | template = indent(@precompile(), 4)
|
329 |
|
330 | for param, module of @findDependencies(template)
|
331 | modules.push module
|
332 | params.push param
|
333 |
|
334 | if modules.length isnt 0
|
335 | modules = for m in modules
|
336 | "'#{ m }'"
|
337 |
|
338 | modules = "[#{ modules }], (#{ params.join(', ') })"
|
339 | else
|
340 | modules = ''
|
341 |
|
342 | """
|
343 | define #{ modules } ->
|
344 | (context) ->
|
345 | render = ->
|
346 | \n#{ template }
|
347 | render.call(context)
|
348 | """
|
349 |
|
350 |
|
351 |
|
352 |
|
353 |
|
354 |
|
355 |
|
356 |
|
357 |
|
358 | renderGlobal: (templateName, namespace = 'window.HAML') ->
|
359 | template = ''
|
360 |
|
361 |
|
362 |
|
363 | segments = "#{ namespace }.#{ templateName }".replace(/(\s|-)+/g, '_').split(/\./)
|
364 | templateName = if @options.basename then segments.pop().split(/\/|\\/).pop() else segments.pop()
|
365 | namespace = segments.shift()
|
366 |
|
367 |
|
368 | if segments.length isnt 0
|
369 | for segment in segments
|
370 | namespace += ".#{ segment }"
|
371 | template += "#{ namespace } ?= {}\n"
|
372 | else
|
373 | template += "#{ namespace } ?= {}\n"
|
374 |
|
375 |
|
376 | if @options.extendScope
|
377 | template += "#{ namespace }['#{ templateName }'] = (context) -> ( ->\n"
|
378 | template += " `with (context || {}) {`\n"
|
379 | template += "#{ indent(@precompile(), 1) }"
|
380 | template += "`}`\n"
|
381 | template += ").call(context)"
|
382 |
|
383 |
|
384 | else
|
385 | template += "#{ namespace }['#{ templateName }'] = (context) -> ( ->\n"
|
386 | template += "#{ indent(@precompile(), 1) }"
|
387 | template += ").call(context)"
|
388 |
|
389 | template
|
390 |
|
391 |
|
392 |
|
393 |
|
394 |
|
395 |
|
396 | precompile: ->
|
397 | fn = ''
|
398 | code = @createCode()
|
399 |
|
400 |
|
401 | if code.indexOf('$e') isnt -1
|
402 | if @options.customHtmlEscape
|
403 | fn += "$e = #{ @options.customHtmlEscape }\n"
|
404 | else
|
405 | fn += """
|
406 | $e = (text, escape) ->
|
407 | "\#{ text }"
|
408 | .replace(/&/g, '&')
|
409 | .replace(/</g, '<')
|
410 | .replace(/>/g, '>')
|
411 | .replace(/\'/g, ''')
|
412 | .replace(/\\//g, '/')
|
413 | .replace(/\"/g, '"')\n
|
414 | """
|
415 |
|
416 |
|
417 | if code.indexOf('$c') isnt -1
|
418 | if @options.customCleanValue
|
419 | fn += "$c = #{ @options.customCleanValue }\n"
|
420 | else
|
421 | fn += "$c = (text) ->\n"
|
422 | fn += " switch text\n"
|
423 | fn += " when null, undefined then ''\n"
|
424 | fn += " when true, false then '\u0093' + text\n"
|
425 | fn += " else text\n"
|
426 |
|
427 |
|
428 | if code.indexOf('$p') isnt -1 || code.indexOf('$fp') isnt -1
|
429 | if @options.customPreserve
|
430 | fn += "$p = #{ @options.customPreserve }\n"
|
431 | else
|
432 | fn += "$p = (text) -> text.replace /\\n/g, '
'\n"
|
433 |
|
434 |
|
435 | if code.indexOf('$fp') isnt -1
|
436 | if @options.customFindAndPreserve
|
437 | fn += "$fp = #{ @options.customFindAndPreserve }\n"
|
438 | else
|
439 | fn +=
|
440 | """
|
441 | $fp = (text) ->
|
442 | text.replace /<(#{ @options.preserveTags.split(',').join('|') })>([^]*?)<\\/\\1>/g, (str, tag, content) ->
|
443 | "<\#{ tag }>\#{ $p content }</\#{ tag }>"\n
|
444 | """
|
445 |
|
446 |
|
447 | if code.indexOf('surround') isnt -1
|
448 | if @options.customSurround
|
449 | fn += "surround = (start, end, fn) => #{ @options.customSurround }.call(@, start, end, fn)\n"
|
450 | else
|
451 | fn += "surround = (start, end, fn) => start + fn.call(@)?.replace(/^\s+|\s+$/g, '') + end\n"
|
452 |
|
453 |
|
454 | if code.indexOf('succeed') isnt -1
|
455 | if @options.customSucceed
|
456 | fn += "succeed = (start, end, fn) => #{ @options.customSucceed }.call(@, start, end, fn)\n"
|
457 | else
|
458 | fn += "succeed = (end, fn) => fn.call(@)?.replace(/\s+$/g, '') + end\n"
|
459 |
|
460 |
|
461 | if code.indexOf('precede') isnt -1
|
462 | if @options.customPrecede
|
463 | fn += "precede = (start, end, fn) => #{ @options.customPrecede }.call(@, start, end, fn)\n"
|
464 | else
|
465 | fn += "precede = (start, fn) => start + fn.call(@)?.replace(/^\s+/g, '')\n"
|
466 |
|
467 |
|
468 | if code.indexOf('$r') isnt -1
|
469 | if @options.customReference
|
470 | fn += "$r = #{ @options.customReference }\n"
|
471 | else
|
472 | fn += """
|
473 | $r = (object, prefix) ->
|
474 | name = if prefix then prefix + '_' else ''
|
475 |
|
476 | if typeof(object.hamlObjectRef) is 'function'
|
477 | name += object.hamlObjectRef()
|
478 | else
|
479 | name += (object.constructor?.name || 'object').replace(/\W+/g, '_').replace(/([a-z\d])([A-Z])/g, '$1_$2').toLowerCase()
|
480 |
|
481 | id = if typeof(object.to_key) is 'function'
|
482 | object.to_key()
|
483 | else if typeof(object.id) is 'function'
|
484 | object.id()
|
485 | else if object.id
|
486 | object.id
|
487 | else
|
488 | object
|
489 |
|
490 | result = "class='\#{ name }'"
|
491 | result += " id='\#{ name }_\#{ id }'" if id\n
|
492 | """
|
493 |
|
494 | fn += "$o = []\n"
|
495 | fn += "#{ code }\n"
|
496 | fn += "return $o.join(\"\\n\")#{ @convertBooleans(code) }#{ @removeEmptyIDAndClass(code) }#{ @cleanupWhitespace(code) }\n"
|
497 |
|
498 |
|
499 |
|
500 |
|
501 |
|
502 |
|
503 |
|
504 |
|
505 | createCode: ->
|
506 | code = []
|
507 |
|
508 | @lines = []
|
509 | @lines = @lines.concat(child.render()) for child in @root.children
|
510 | @lines = @combineText(@lines)
|
511 |
|
512 | @blockLevel = 0
|
513 |
|
514 | for line in @lines
|
515 | unless line is null
|
516 | switch line.type
|
517 |
|
518 |
|
519 | when 'text'
|
520 | code.push "#{ whitespace(line.cw) }#{ @getBuffer(@blockLevel) }.push \"#{ whitespace(line.hw) }#{ line.text }\""
|
521 |
|
522 |
|
523 | when 'run'
|
524 | if line.block isnt 'end'
|
525 | code.push "#{ whitespace(line.cw) }#{ line.code }"
|
526 |
|
527 | else
|
528 | code.push "#{ whitespace(line.cw) }#{ line.code.replace '$buffer', @getBuffer(@blockLevel) }"
|
529 | @blockLevel -= 1
|
530 |
|
531 |
|
532 | when 'insert'
|
533 | processors = ''
|
534 | processors += '$fp ' if line.findAndPreserve
|
535 | processors += '$p ' if line.preserve
|
536 | processors += '$e ' if line.escape
|
537 | processors += '$c ' if @options.cleanValue
|
538 |
|
539 | code.push "#{ whitespace(line.cw) }#{ @getBuffer(@blockLevel) }.push \"#{ whitespace(line.hw) }\" + #{ processors }#{ line.code }"
|
540 |
|
541 |
|
542 | if line.block is 'start'
|
543 | @blockLevel += 1
|
544 | code.push "#{ whitespace(line.cw + 1) }#{ @getBuffer(@blockLevel) } = []"
|
545 |
|
546 | code.join '\n'
|
547 |
|
548 |
|
549 |
|
550 |
|
551 |
|
552 | getBuffer: (level) ->
|
553 | if level > 0 then "$o#{ level }" else '$o'
|
554 |
|
555 |
|
556 |
|
557 |
|
558 |
|
559 |
|
560 |
|
561 | combineText: (lines) ->
|
562 | combined = []
|
563 |
|
564 | while (line = lines.shift()) isnt undefined
|
565 | if line.type is 'text'
|
566 | while lines[0] and lines[0].type is 'text' and line.cw is lines[0].cw
|
567 | nextLine = lines.shift()
|
568 | line.text += "\\n#{ whitespace(nextLine.hw) }#{ nextLine.text }"
|
569 |
|
570 | combined.push line
|
571 |
|
572 | combined
|
573 |
|
574 |
|
575 |
|
576 |
|
577 |
|
578 |
|
579 |
|
580 |
|
581 |
|
582 |
|
583 |
|
584 |
|
585 |
|
586 |
|
587 |
|
588 |
|
589 |
|
590 |
|
591 |
|
592 | convertBooleans: (code) ->
|
593 | if code.indexOf('$c') isnt -1
|
594 | if @options.format is 'xhtml'
|
595 | '.replace(/\\s(\\w+)=\'\u0093true\'/mg, " $1=\'$1\'").replace(/\\s(\\w+)=\'\u0093false\'/mg, \'\')'
|
596 | else
|
597 | '.replace(/\\s(\\w+)=\'\u0093true\'/mg, \' $1\').replace(/\\s(\\w+)=\'\u0093false\'/mg, \'\')'
|
598 | else
|
599 | ''
|
600 |
|
601 |
|
602 |
|
603 |
|
604 |
|
605 |
|
606 |
|
607 |
|
608 | removeEmptyIDAndClass: (code) ->
|
609 | if code.indexOf('id=') isnt -1 || code.indexOf('class=') isnt -1
|
610 | '.replace(/\\s(?:id|class)=([\'"])(\\1)/mg, "")'
|
611 | else
|
612 | ''
|
613 |
|
614 |
|
615 |
|
616 |
|
617 |
|
618 |
|
619 |
|
620 |
|
621 |
|
622 |
|
623 |
|
624 |
|
625 |
|
626 |
|
627 | cleanupWhitespace: (code) ->
|
628 | if /\u0091|\u0092/.test code
|
629 | ".replace(/[\\s\\n]*\\u0091/mg, '').replace(/\\u0092[\\s\\n]*/mg, '')"
|
630 | else
|
631 | ''
|
632 |
|
633 |
|
634 |
|
635 |
|
636 |
|
637 |
|
638 |
|
639 |
|
640 |
|
641 |
|
642 | findDependencies: (code) ->
|
643 | requireRegexp = /require(?:\s+|\()(['"])(.+?)(\1)\)?/gm
|
644 | dependencies = {}
|
645 |
|
646 | while match = requireRegexp.exec code
|
647 | module = match[2]
|
648 | name = module.split('/').pop()
|
649 |
|
650 | dependencies[name] = module
|
651 |
|
652 | dependencies
|