lib: allow multiple class definition in html elements
[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 self.tag = tag
97
98 init with_attrs(tag: String, attrs: Map[String, String]) do
99 self.tag = tag
100 self.attrs = attrs
101 end
102
103 # Tag attributes map
104 var attrs: Map[String, String] = new HashMap[String, String]
105
106 # Get the attributed value of 'prop' or null if 'prop' is undifened
107 fun get_attr(key: String): nullable String do
108 if not attrs.has_key(key) then return null
109 return attrs[key]
110 end
111
112 # Set a 'value' for 'key'
113 # var img = new HTMLTag("img")
114 # img.attr("src", "./image.png").attr("alt", "image")
115 fun attr(key: String, value: String): HTMLTag do
116 attrs[key] = value
117 return self
118 end
119
120 # Add a CSS class to the HTML tag
121 # var img = new HTMLTag("img")
122 # img.add_class("logo").add_class("fullpage")
123 fun add_class(klass: String): HTMLTag do
124 classes.add(klass)
125 return self
126 end
127 private var classes: Set[String] = new HashSet[String]
128
129 # Add multiple CSS classes
130 fun add_classes(classes: Collection[String]): HTMLTag do
131 self.classes.add_all(classes)
132 return self
133 end
134
135 # Set a CSS 'value' for 'prop'
136 # var img = new HTMLTag("img")
137 # img.css("border", "2px solid black").css("position", "absolute")
138 fun css(prop: String, value: String): HTMLTag do
139 css_props[prop] = value
140 return self
141 end
142 private var css_props: Map[String, String] = new HashMap[String, String]
143
144 # Get CSS value for 'prop'
145 fun get_css(prop: String): nullable String do
146 if not css_props.has_key(prop) then return null
147 return css_props[prop]
148 end
149
150 # Add a HTML 'child' to self
151 # var ul = new HTMLTag("ul")
152 # ul.add(new HTMLTag("li"))
153 fun add(child: HTMLTag) do children.add(child)
154
155 # List of children HTML elements
156 var children: Set[HTMLTag] = new HashSet[HTMLTag]
157
158 # Set text of element
159 # var p = new HTMLTag("p")
160 # p.text("Hello World!")
161 # Text is escaped see: standard::String::html_escape
162 fun text(txt: String): HTMLTag do
163 content.clear
164 content.append(txt)
165 return self
166 end
167 private var content = new Buffer
168
169 # Append text to element
170 # var p = new HTMLTag("p")
171 # p.append("Hello").append("<br/>").append("World!")
172 # Text is escaped see: standard::String::html_escape
173 fun append(txt: String): HTMLTag do
174 content.append(txt)
175 return self
176 end
177
178 # Render the element as HTML string
179 fun html: String do
180 var attrs = render_attrs
181 var content = render_content
182 var str = new Buffer
183 str.append("<{tag}")
184 str.append(attrs)
185 if tag != "script" and content.is_empty then
186 str.append("/>")
187 else
188 str.append(">")
189 str.append(content)
190 str.append("</{tag}>")
191 end
192 return str.to_s
193 end
194
195 private fun render_attrs: String do
196 var cls = render_classes
197 var css = render_css
198 var str = new Buffer
199 if not cls.is_empty then
200 str.append(" ")
201 str.append(render_classes)
202 end
203 if not css.is_empty then
204 str.append(" ")
205 str.append(render_css)
206 end
207 if not attrs.is_empty then str.append(" ")
208 var count = 0
209 for key, value in attrs do
210 if key == "class" or key == "style" then continue
211 str.append("{key}=\"{value}\"")
212 if count < attrs.length - 1 then
213 str.append(" ")
214 end
215 count += 1
216 end
217 return str.to_s
218 end
219
220 private fun render_css: String do
221 if not attrs.has_key("style") and css_props.is_empty then return ""
222 var css = new Buffer
223 css.append("style=\"")
224 if attrs.has_key("style") then css.append("{attrs["style"]}; ")
225 css.append(css_props.join("; ", ": "))
226 css.append("\"")
227 return css.to_s
228 end
229
230 private fun render_classes: String do
231 if not attrs.has_key("class") and classes.is_empty then return ""
232 var cls = new Buffer
233 cls.append("class=\"")
234 if attrs.has_key("class") then cls.append("{attrs["class"]} ")
235 cls.append(classes.join(" "))
236 cls.append("\"")
237 return cls.to_s
238 end
239
240 private fun render_content: String do
241 var str = new Buffer
242 str.append(content.to_s.html_escape)
243 for child in children do
244 str.append(child.html)
245 end
246 return str.to_s
247 end
248 end
249
250 private class HTMLRaw
251 super HTMLTag
252
253 init(content: String) do self.content.append(content)
254 redef fun html do return content.to_s
255 end