lib: document abstract_collection + nitunit tests
[nit.git] / lib / 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 class HTMLTag
88 super Streamable
89
90 # HTML tagname: 'div' for <div></div>
91 var tag: String
92 init(tag: String) do
93 self.tag = tag
94 self.is_void = (once ["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr"]).has(tag)
95 end
96
97 # Is the HTML element a void element?
98 var is_void: Bool
99
100 init with_attrs(tag: String, attrs: Map[String, String]) do
101 self.tag = tag
102 self.attrs = attrs
103 end
104
105 # Tag attributes map
106 var attrs: Map[String, String] = new HashMap[String, String]
107
108 # Get the attributed value of 'prop' or null if 'prop' is undifened
109 fun get_attr(key: String): nullable String do
110 if not attrs.has_key(key) then return null
111 return attrs[key]
112 end
113
114 # Set a 'value' for 'key'
115 # var img = new HTMLTag("img")
116 # img.attr("src", "./image.png").attr("alt", "image")
117 fun attr(key: String, value: String): HTMLTag do
118 attrs[key] = value
119 return self
120 end
121
122 # Add a CSS class to the HTML tag
123 # var img = new HTMLTag("img")
124 # img.add_class("logo").add_class("fullpage")
125 fun add_class(klass: String): HTMLTag do
126 classes.add(klass)
127 return self
128 end
129 var classes: Set[String] = new HashSet[String]
130
131 # Add multiple CSS classes
132 fun add_classes(classes: Collection[String]): HTMLTag do
133 self.classes.add_all(classes)
134 return self
135 end
136
137 # Set a CSS 'value' for 'prop'
138 # var img = new HTMLTag("img")
139 # img.css("border", "2px solid black").css("position", "absolute")
140 fun css(prop: String, value: String): HTMLTag do
141 css_props[prop] = value
142 return self
143 end
144 private var css_props: Map[String, String] = new HashMap[String, String]
145
146 # Get CSS value for 'prop'
147 fun get_css(prop: String): nullable String do
148 if not css_props.has_key(prop) then return null
149 return css_props[prop]
150 end
151
152 # Add a HTML 'child' to self
153 # var ul = new HTMLTag("ul")
154 # ul.add(new HTMLTag("li"))
155 fun add(child: HTMLTag) do children.add(child)
156
157 # List of children HTML elements
158 var children: Set[HTMLTag] = new HashSet[HTMLTag]
159
160 # Clear all child and set the text of element
161 # var p = new HTMLTag("p")
162 # p.text("Hello World!")
163 # assert p.write_to_string == "<p>Hello World!</p>"
164 # Text is escaped see: `standard::String::html_escape`
165 fun text(txt: String): HTMLTag do
166
167 children.clear
168 append(txt)
169 return self
170 end
171
172 # Append text to element
173 # var p = new HTMLTag("p")
174 # p.append("Hello")
175 # p.add(new HTMLTag("br"))
176 # p.append("World!")
177 # assert p.write_to_string == "<p>Hello<br/>World!</p>"
178 # Text is escaped see: standard::String::html_escape
179 fun append(txt: String): HTMLTag do
180 add(new HTMLRaw(txt.html_escape))
181 return self
182 end
183
184 # Append raw HTML to element
185 # var p = new HTMLTag("p")
186 # p.append("Hello")
187 # p.add_raw_html("<bla/>")
188 # p.html #- "<p>Hello<bla/></p>"
189 # Note: the HTML in insered as it, no verification is done
190 fun add_raw_html(txt: String): HTMLTag do
191 add(new HTMLRaw(txt))
192 return self
193 end
194
195 redef fun write_to(stream) do
196 var res = new Array[String]
197 render_in(res)
198 for r in res do
199 stream.write(r)
200 end
201 end
202
203 # In order to avoid recursive concatenation,
204 # this function collects in `res` all the small fragments of `String`
205 private fun render_in(res: Sequence[String])
206 do
207 res.add "<"
208 res.add tag
209 render_attrs_in(res)
210 if is_void and children.is_empty then
211 res.add "/>"
212 else
213 res.add ">"
214 for child in children do child.render_in(res)
215 res.add "</"
216 res.add tag
217 res.add ">"
218 end
219 end
220
221 private fun render_attrs_in(res: Sequence[String]) do
222 if attrs.has_key("class") or not classes.is_empty then
223 res.add " class=\""
224 for cls in classes do
225 res.add cls.html_escape
226 res.add " "
227 end
228 if attrs.has_key("class") then
229 res.add attrs["class"].html_escape
230 res.add " "
231 end
232 if res.last == " " then res.pop
233 res.add "\""
234 end
235
236 if attrs.has_key("style") or not css_props.is_empty then
237 res.add " style=\""
238 for k, v in attrs do
239 res.add k.html_escape
240 res.add ": "
241 res.add v.html_escape
242 res.add "; "
243 end
244 if attrs.has_key("style") then
245 res.add(attrs["style"].html_escape)
246 end
247 if res.last == "; " then res.pop
248 res.add "\""
249 end
250
251 if attrs.is_empty then return
252
253 for key, value in attrs do
254 if key == "class" or key == "style" then continue
255 res.add " "
256 res.add key.html_escape
257 res.add "=\""
258 res.add value.html_escape
259 res.add "\""
260 end
261 end
262 end
263
264 private class HTMLRaw
265 super HTMLTag
266
267 private var content: String
268 init(content: String) do self.content = content
269 redef fun render_in(res) do res.add content
270 end