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