lib/html: implement the tag list as an hashset instead of a array.
[nit.git] / lib / html / html.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 # HTML output facilities
16 module html
17
18 # A html page
19 #
20 # You can define subclass and override methods head and body
21 #
22 # ~~~nitish
23 # class MyPage
24 # super HTMLPage
25 # redef body do add("p").text("Hello World!")
26 # end
27 # ~~~
28 #
29 # HTMLPage use fluent interface so you can chain calls as:
30 #
31 # ~~~nitish
32 # add("div").attr("id", "mydiv").text("My Div")
33 # ~~~
34 class HTMLPage
35 super Writable
36
37 # Define head content
38 fun head do end
39 # Define body content
40 fun body do end
41
42 private var root = new HTMLTag("html")
43 private var current: HTMLTag = root
44 private var stack = new List[HTMLTag]
45
46 redef fun write_to(stream) do
47 root.children.clear
48 open("head")
49 head
50 close("head")
51 open("body")
52 body
53 close("body")
54 stream.write "<!DOCTYPE html>"
55 root.write_to(stream)
56 end
57
58 # Add a html tag to the current element
59 #
60 # ~~~nitish
61 # add("div").attr("id", "mydiv").text("My Div")
62 # ~~~
63 fun add(tag: String): HTMLTag do
64 var node = new HTMLTag(tag)
65 current.add(node)
66 return node
67 end
68
69 # Add a raw html string
70 #
71 # ~~~nitish
72 # add_html("<a href='#top'>top</a>")
73 # ~~~
74 fun add_html(html: String) do current.add(new HTMLRaw("", html))
75
76 # Open a html tag
77 #
78 # ~~~nitish
79 # open("ul")
80 # add("li").text("item1")
81 # add("li").text("item2")
82 # close("ul")
83 # ~~~
84 fun open(tag: String): HTMLTag do
85 stack.push(current)
86 current = add(tag)
87 return current
88 end
89
90 # Close previously opened tag
91 # Ensure: tag = previous.tag
92 fun close(tag: String) do
93 if not tag == current.tag then
94 print "Error: Trying to close '{tag}', last opened tag was '{current.tag}'."
95 abort
96 end
97 current = stack.pop
98 end
99 end
100
101 # An HTML element.
102 class HTMLTag
103 super Writable
104
105 # HTML element type.
106 #
107 # `"div"` for `<div></div>`.
108 var tag: String
109 init do
110 self.is_void = (once void_list).has(tag)
111 end
112
113 private fun void_list: Set[String]
114 do
115 return new HashSet[String].from(["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr"])
116 end
117
118 # Is the HTML element a void element?
119 #
120 # assert (new HTMLTag("img")).is_void == true
121 # assert (new HTMLTag("p")).is_void == false
122 var is_void: Bool is noinit
123
124 # Create a HTML elements with the specifed type and attributes.
125 init with_attrs(tag: String, attrs: Map[String, String]) do
126 self.tag = tag
127 self.attrs = attrs
128 end
129
130 # Tag attributes map
131 var attrs: Map[String, String] = new HashMap[String, String]
132
133 # Get the attributed value of 'prop' or null if 'prop' is undifened
134 #
135 # var img = new HTMLTag("img")
136 # img.attr("src", "./image.png").attr("alt", "image")
137 # assert img.get_attr("src") == "./image.png"
138 fun get_attr(key: String): nullable String do
139 if not attrs.has_key(key) then return null
140 return attrs[key]
141 end
142
143 # Set a 'value' for 'key'
144 #
145 # var img = new HTMLTag("img")
146 # img.attr("src", "./image.png").attr("alt", "image")
147 # assert img.write_to_string == """<img src=".&#47;image.png" alt="image"/>"""
148 fun attr(key: String, value: String): HTMLTag do
149 attrs[key] = value
150 return self
151 end
152
153 # Add a CSS class to the HTML tag
154 #
155 # var img = new HTMLTag("img")
156 # img.add_class("logo").add_class("fullpage")
157 # assert img.write_to_string == """<img class="logo fullpage"/>"""
158 fun add_class(klass: String): HTMLTag do
159 classes.add(klass)
160 return self
161 end
162
163 # CSS classes
164 var classes: Set[String] = new HashSet[String]
165
166 # Add multiple CSS classes
167 #
168 # var img = new HTMLTag("img")
169 # img.add_classes(["logo", "fullpage"])
170 # assert img.write_to_string == """<img class="logo fullpage"/>"""
171 fun add_classes(classes: Collection[String]): HTMLTag do
172 self.classes.add_all(classes)
173 return self
174 end
175
176 # Set a CSS 'value' for 'prop'
177 #
178 # var img = new HTMLTag("img")
179 # img.css("border", "2px solid black").css("position", "absolute")
180 # assert img.write_to_string == """<img style="border: 2px solid black; position: absolute"/>"""
181 fun css(prop: String, value: String): HTMLTag do
182 css_props[prop] = value
183 return self
184 end
185 private var css_props: Map[String, String] = new HashMap[String, String]
186
187 # Get CSS value for 'prop'
188 #
189 # var img = new HTMLTag("img")
190 # img.css("border", "2px solid black").css("position", "absolute")
191 # assert img.get_css("border") == "2px solid black"
192 # assert img.get_css("color") == null
193 fun get_css(prop: String): nullable String do
194 if not css_props.has_key(prop) then return null
195 return css_props[prop]
196 end
197
198 # Replace `self` by `parent`.
199 #
200 # var elem = new HTMLTag("li")
201 # elem.add_outer(new HTMLTag("ul"))
202 # assert elem.write_to_string == "<ul><li></li></ul>"
203 fun add_outer(parent: HTMLTag) do
204 # copy self in new object
205 var child = new HTMLTag(self.tag)
206 child.attrs = self.attrs
207 child.classes = self.classes
208 child.css_props = self.css_props
209 child.children = self.children
210 # add copy in parent children elements
211 parent.children.add(child)
212 # replace self by parent
213 self.tag = parent.tag
214 self.attrs = parent.attrs
215 self.classes = parent.classes
216 self.css_props = parent.css_props
217 self.is_void = parent.is_void
218 self.children = parent.children
219 end
220
221 # Add a HTML 'child' to self
222 #
223 # var ul = new HTMLTag("ul")
224 # ul.add(new HTMLTag("li"))
225 # assert ul.write_to_string == "<ul><li></li></ul>"
226 # returns `self` for fluent programming
227 fun add(child: HTMLTag): HTMLTag
228 do
229 children.add(child)
230 return self
231 end
232
233 # Create a new HTMLTag child and return it
234 #
235 # var ul = new HTMLTag("ul")
236 # ul.open("li").append("1").append("2")
237 # ul.open("li").append("3").append("4")
238 # assert ul.write_to_string == "<ul><li>12</li><li>34</li></ul>"
239 fun open(tag: String): HTMLTag
240 do
241 var res = new HTMLTag(tag)
242 add(res)
243 return res
244 end
245
246 # List of children HTML elements
247 var children: Set[HTMLTag] = new HashSet[HTMLTag]
248
249 # Clear all child and set the text of element
250 #
251 # var p = new HTMLTag("p")
252 # p.text("Hello World!")
253 # assert p.write_to_string == "<p>Hello World!</p>"
254 # Text is escaped see: `core::String::html_escape`
255 fun text(txt: String): HTMLTag do
256
257 children.clear
258 append(txt)
259 return self
260 end
261
262 # Append text to element
263 #
264 # var p = new HTMLTag("p")
265 # p.append("Hello")
266 # p.add(new HTMLTag("br"))
267 # p.append("World!")
268 # assert p.write_to_string == "<p>Hello<br/>World!</p>"
269 # Text is escaped see: core::String::html_escape
270 fun append(txt: String): HTMLTag do
271 add(new HTMLRaw("", txt.html_escape))
272 return self
273 end
274
275 # Append raw HTML to element
276 #
277 # var p = new HTMLTag("p")
278 # p.append("Hello")
279 # p.add_raw_html("<bla/>foo")
280 # assert p.write_to_string == "<p>Hello<bla/>foo</p>"
281 #
282 # Note: the HTML in insered as it, no verification is done.
283 fun add_raw_html(txt: String): HTMLTag do
284 add(new HTMLRaw("", txt))
285 return self
286 end
287
288 redef fun write_to(stream) do
289 var res = new Array[String]
290 render_in(res)
291 for r in res do
292 stream.write(r)
293 end
294 end
295
296 # In order to avoid recursive concatenation,
297 # this function collects in `res` all the small fragments of `String`
298 private fun render_in(res: Sequence[String])
299 do
300 res.add "<"
301 res.add tag
302 render_attrs_in(res)
303 if is_void and children.is_empty then
304 res.add "/>"
305 else
306 res.add ">"
307 for child in children do child.render_in(res)
308 res.add "</"
309 res.add tag
310 res.add ">"
311 end
312 end
313
314 private fun render_attrs_in(res: Sequence[String]) do
315 if attrs.has_key("class") or not classes.is_empty then
316 res.add " class=\""
317 for cls in classes do
318 res.add cls.html_escape
319 res.add " "
320 end
321 if attrs.has_key("class") then
322 res.add attrs["class"].html_escape
323 res.add " "
324 end
325 if res.last == " " then res.pop
326 res.add "\""
327 end
328
329 if attrs.has_key("style") or not css_props.is_empty then
330 res.add " style=\""
331 for k, v in css_props do
332 res.add k.html_escape
333 res.add ": "
334 res.add v.html_escape
335 res.add "; "
336 end
337 if attrs.has_key("style") then
338 res.add(attrs["style"].html_escape)
339 end
340 if res.last == "; " then res.pop
341 res.add "\""
342 end
343
344 if attrs.is_empty then return
345
346 for key, value in attrs do
347 if key == "class" or key == "style" then continue
348 res.add " "
349 res.add key.html_escape
350 res.add "=\""
351 res.add value.html_escape
352 res.add "\""
353 end
354 end
355 end
356
357 private class HTMLRaw
358 super HTMLTag
359
360 var content: String
361 redef fun render_in(res) do res.add content
362 end