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