1 | {escapeHTML} = require('../util/text')
|
2 |
|
3 | # Base class for the syntax tree.
|
4 | #
|
5 | # This will provide some methods that subclasses must use in order to generate
|
6 | # some output:
|
7 | #
|
8 | # * {#markText}
|
9 | # * {#markRunningCode}
|
10 | # * {#markInsertingCode}
|
11 | #
|
12 | # Each node must mark the `@opener` attribute and can optionally mark the `@closer`
|
13 | # attribute.
|
14 | #
|
15 | # @abstract
|
16 | #
|
17 | module.exports = class Node
|
18 |
|
19 | # Hidden unicode marker to remove left whitespace after template rendering.
|
20 | @CLEAR_WHITESPACE_LEFT = '\u0091'
|
21 |
|
22 | # Hidden unicode marker to remove right whitespace after template rendering.
|
23 | @CLEAR_WHITESPACE_RIGHT = '\u0092'
|
24 |
|
25 | # Constructs a syntax node.
|
26 | #
|
27 | # @param [String] expression the Haml expression to evaluate
|
28 | # @param [Object] options the node options
|
29 | # @option options [Node] parentNode the parent node
|
30 | # @option options [Number] blockLevel the HTML block level
|
31 | # @option options [Number] codeBlockLevel the CoffeeScript block level
|
32 | # @option options [Boolean] escapeHtml whether to escape the rendered HTML or not
|
33 | # @option options [String] format the template format, either `xhtml`, `html4` or `html5`
|
34 | #
|
35 | constructor: (@expression = '', options = {}) ->
|
36 | @parentNode = options.parentNode
|
37 | @children = []
|
38 |
|
39 | @opener = @closer = null
|
40 |
|
41 | # A silent node swallows all output
|
42 | @silent = false
|
43 |
|
44 | # Preserve whitespace on all children
|
45 | @preserveTags = options.preserveTags.split(',')
|
46 | @preserve = false
|
47 |
|
48 | @wsRemoval = {
|
49 | around: false
|
50 | inside: false
|
51 | }
|
52 |
|
53 | @escapeHtml = options.escapeHtml
|
54 | @escapeAttributes = options.escapeAttributes
|
55 | @cleanValue = options.cleanValue
|
56 | @format = options.format
|
57 | @hyphenateDataAttrs = options.hyphenateDataAttrs
|
58 | @selfCloseTags = options.selfCloseTags.split(',')
|
59 | @uglify = options.uglify
|
60 |
|
61 | @codeBlockLevel = options.codeBlockLevel
|
62 | @blockLevel = options.blockLevel
|
63 |
|
64 | @placement = options.placement
|
65 | @namespace = options.namespace
|
66 | @name = options.name
|
67 |
|
68 | # Add a child node.
|
69 | #
|
70 | # @param [Node] child the child node
|
71 | #
|
72 | addChild: (child) ->
|
73 | @children.push child
|
74 | @
|
75 |
|
76 | # Get the opening tag for the node.
|
77 | #
|
78 | # This may add a hidden unicode control character for
|
79 | # later whitespace processing:
|
80 | #
|
81 | # * `\u0091` Cleanup surrounding whitespace to the left
|
82 | # * `\u0092` Cleanup surrounding whitespace to the right
|
83 | #
|
84 | # @return [String] the opening tag
|
85 | #
|
86 | getOpener: ->
|
87 | @opener.text = Node.CLEAR_WHITESPACE_LEFT + @opener.text if @wsRemoval.around and @opener.text
|
88 | @opener.text += Node.CLEAR_WHITESPACE_RIGHT if @wsRemoval.inside and @opener.text
|
89 |
|
90 | @opener
|
91 |
|
92 | # Get the closing tag for the node.
|
93 | #
|
94 | # This may add a hidden unicode control character for
|
95 | # later whitespace processing:
|
96 | #
|
97 | # * `\u0091` Cleanup surrounding whitespace to the left
|
98 | # * `\u0092` Cleanup surrounding whitespace to the right
|
99 | #
|
100 | # @return [String] the closing tag
|
101 | #
|
102 | getCloser: ->
|
103 | @closer.text = Node.CLEAR_WHITESPACE_LEFT + @closer.text if @wsRemoval.inside and @closer.text
|
104 | @closer.text += Node.CLEAR_WHITESPACE_RIGHT if @wsRemoval.around and @closer.text
|
105 |
|
106 | @closer
|
107 |
|
108 | # Traverse up the tree to see if a parent node
|
109 | # is preserving output space.
|
110 | #
|
111 | # @return [Boolean] true when preserved
|
112 | #
|
113 | isPreserved: ->
|
114 | return true if @preserve
|
115 |
|
116 | if @parentNode
|
117 | @parentNode.isPreserved()
|
118 | else
|
119 | false
|
120 |
|
121 | # Traverse up the tree to see if a parent node
|
122 | # is a comment node.
|
123 | #
|
124 | # @return [Boolean] true when within a comment
|
125 | #
|
126 | isCommented: ->
|
127 | return true if @constructor.name is 'Comment'
|
128 |
|
129 | if @parentNode
|
130 | @parentNode.isCommented()
|
131 | else
|
132 | false
|
133 |
|
134 | # Creates a marker for static outputted text.
|
135 | #
|
136 | # @param [String] html the html to output
|
137 | # @param [Boolean] escape whether to escape the generated output
|
138 | # @return [Object] the marker
|
139 | #
|
140 | markText: (text, escape = false) ->
|
141 | {
|
142 | type : 'text'
|
143 | cw : @codeBlockLevel
|
144 | hw : if @uglify then 0 else @blockLevel - @codeBlockLevel
|
145 | text : if escape then escapeHTML(text) else text
|
146 | }
|
147 |
|
148 | # Creates a marker for running CoffeeScript
|
149 | # code that doesn't generate any output.
|
150 | #
|
151 | # @param [String] code the CoffeeScript code
|
152 | # @return [Object] the marker
|
153 | #
|
154 | markRunningCode: (code) ->
|
155 | {
|
156 | type : 'run'
|
157 | cw : @codeBlockLevel
|
158 | code : code
|
159 | }
|
160 |
|
161 | # Creates a marker for inserting CoffeeScript
|
162 | # code that generate an output.
|
163 | #
|
164 | # @param [String] code the CoffeeScript code
|
165 | # @param [Boolean] escape whether to escape the generated output
|
166 | # @param [Boolean] preserve when preserve all newlines
|
167 | # @param [Boolean] findAndPreserve when preserve newlines within preserved tags
|
168 | # @return [Object] the marker
|
169 | #
|
170 | markInsertingCode: (code, escape = false, preserve = false, findAndPreserve = false) ->
|
171 | {
|
172 | type : 'insert'
|
173 | cw : @codeBlockLevel
|
174 | hw : if @uglify then 0 else @blockLevel - @codeBlockLevel
|
175 | escape : escape
|
176 | preserve : preserve
|
177 | findAndPreserve : findAndPreserve
|
178 | code : code
|
179 | }
|
180 |
|
181 | # Template method that must be implemented by each
|
182 | # Node subclass. This evaluates the `@expression`
|
183 | # and save marks the output type on the `@opener` and
|
184 | # `@closer` attributes if applicable.
|
185 | #
|
186 | # @abstract
|
187 | #
|
188 | evaluate: ->
|
189 |
|
190 | # Render the node and its children.
|
191 | #
|
192 | # Always use `@opener` and `@closer` for content checks,
|
193 | # but `@getOpener()` and `@getCloser()` for outputting,
|
194 | # because they may contain whitespace removal control
|
195 | # characters.
|
196 | #
|
197 | # @return [Array] all markers
|
198 | #
|
199 | render: ->
|
200 | output = []
|
201 |
|
202 | # Swallow child output when silent
|
203 | return output if @silent
|
204 |
|
205 | # Nodes without children
|
206 | if @children.length is 0
|
207 |
|
208 | # Non self closing tag
|
209 | if @opener and @closer
|
210 |
|
211 | # Merge tag into a single line
|
212 | tag = @getOpener()
|
213 | tag.text += @getCloser().text
|
214 |
|
215 | output.push tag
|
216 |
|
217 | # Self closing tag
|
218 | else
|
219 |
|
220 | # Whitespace preserved child tag are outputted by the preserving tag
|
221 | if not @preserve && @isPreserved()
|
222 | output.push @getOpener()
|
223 |
|
224 | # Normal self closing tag
|
225 | else
|
226 | output.push @getOpener()
|
227 |
|
228 | # Nodes with children
|
229 | else
|
230 |
|
231 | # Non self closing Haml tag
|
232 | if @opener and @closer
|
233 |
|
234 | # Whitespace preserving tag combines children into a single line
|
235 | if @preserve
|
236 |
|
237 | # Preserved tags removes the inside whitespace
|
238 | @wsRemoval.inside = true
|
239 |
|
240 | output.push @getOpener()
|
241 |
|
242 | for child in @children
|
243 | for rendered in child.render()
|
244 | # Move all children's block level to the preserving tag
|
245 | rendered.hw = @blockLevel
|
246 | output.push rendered
|
247 |
|
248 | output.push @getCloser()
|
249 |
|
250 | # Non preserving tag
|
251 | else
|
252 | output.push @getOpener()
|
253 | output = output.concat(child.render()) for child in @children
|
254 | output.push @getCloser()
|
255 |
|
256 | # Block with only an opener
|
257 | else if @opener
|
258 | output.push @getOpener()
|
259 | output = output.concat(child.render()) for child in @children
|
260 |
|
261 | # Text and code node or Haml nodes without content
|
262 | else
|
263 | output.push @markText(child.render().text) for child in @children
|
264 |
|
265 | output
|