# HTMLPage use fluent interface so you can chain calls as:
# add("div").attr("id", "mydiv").text("My Div")
class HTMLPage
+ super Streamable
# Define head content
fun head do end
private var current: HTMLTag = root
private var stack = new List[HTMLTag]
- # Render the page as a html string
- fun render: String do
+ redef fun write_to(stream) do
root.children.clear
open("head")
head
open("body")
body
close("body")
- return "<!DOCTYPE html>{root.html}"
+ stream.write "<!DOCTYPE html>"
+ root.write_to(stream)
end
# Add a html tag to the current element
end
current = stack.pop
end
-
- # Save html page in the specified file
- fun save(file: String) do
- var out = new OFStream.open(file)
- out.write(self.render)
- out.close
- end
end
class HTMLTag
+ super Streamable
+
# HTML tagname: 'div' for <div></div>
var tag: String
- init(tag: String) do self.tag = tag
+ init(tag: String) do
+ self.tag = tag
+ self.is_void = (once ["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr"]).has(tag)
+ end
+
+ # Is the HTML element a void element?
+ #
+ # assert (new HTMLTag("img")).is_void == true
+ # assert (new HTMLTag("p")).is_void == false
+ var is_void: Bool
init with_attrs(tag: String, attrs: Map[String, String]) do
self.tag = tag
var attrs: Map[String, String] = new HashMap[String, String]
# Get the attributed value of 'prop' or null if 'prop' is undifened
+ # var img = new HTMLTag("img")
+ # img.attr("src", "./image.png").attr("alt", "image")
+ # assert img.get_attr("src") == "./image.png"
fun get_attr(key: String): nullable String do
if not attrs.has_key(key) then return null
return attrs[key]
end
# Set a 'value' for 'key'
- # var img = new HTMLTag("img")
- # img.attr("src", "./image.png").attr("alt", "image")
+ # var img = new HTMLTag("img")
+ # img.attr("src", "./image.png").attr("alt", "image")
+ # assert img.write_to_string == """<img src="./image.png" alt="image"/>"""
fun attr(key: String, value: String): HTMLTag do
attrs[key] = value
return self
end
# Add a CSS class to the HTML tag
- # var img = new HTMLTag("img")
- # img.add_class("logo").add_class("fullpage")
+ # var img = new HTMLTag("img")
+ # img.add_class("logo").add_class("fullpage")
+ # assert img.write_to_string == """<img class="logo fullpage"/>"""
fun add_class(klass: String): HTMLTag do
classes.add(klass)
return self
end
- private var classes: Set[String] = new HashSet[String]
+
+ # CSS classes
+ var classes: Set[String] = new HashSet[String]
+
+ # Add multiple CSS classes
+ # var img = new HTMLTag("img")
+ # img.add_classes(["logo", "fullpage"])
+ # assert img.write_to_string == """<img class="logo fullpage"/>"""
+ fun add_classes(classes: Collection[String]): HTMLTag do
+ self.classes.add_all(classes)
+ return self
+ end
# Set a CSS 'value' for 'prop'
- # var img = new HTMLTag("img")
- # img.css("border", "2px solid black").css("position", "absolute")
+ # var img = new HTMLTag("img")
+ # img.css("border", "2px solid black").css("position", "absolute")
+ # assert img.write_to_string == """<img style="border: 2px solid black; position: absolute"/>"""
fun css(prop: String, value: String): HTMLTag do
css_props[prop] = value
return self
private var css_props: Map[String, String] = new HashMap[String, String]
# Get CSS value for 'prop'
+ # var img = new HTMLTag("img")
+ # img.css("border", "2px solid black").css("position", "absolute")
+ # assert img.get_css("border") == "2px solid black"
+ # assert img.get_css("color") == null
fun get_css(prop: String): nullable String do
if not css_props.has_key(prop) then return null
return css_props[prop]
end
+ # Replace `self` by `parent`.
+ #
+ # var elem = new HTMLTag("li")
+ # elem.add_outer(new HTMLTag("ul"))
+ # assert elem.write_to_string == "<ul><li></li></ul>"
+ fun add_outer(parent: HTMLTag) do
+ # copy self in new object
+ var child = new HTMLTag(self.tag)
+ child.attrs = self.attrs
+ child.classes = self.classes
+ child.css_props = self.css_props
+ child.children = self.children
+ # add copy in parent children elements
+ parent.children.add(child)
+ # replace self by parent
+ self.tag = parent.tag
+ self.attrs = parent.attrs
+ self.classes = parent.classes
+ self.css_props = parent.css_props
+ self.is_void = parent.is_void
+ self.children = parent.children
+ end
+
# Add a HTML 'child' to self
- # var ul = new HTMLTag("ul")
- # ul.add(new HTMLTag("li"))
- fun add(child: HTMLTag) do children.add(child)
+ # var ul = new HTMLTag("ul")
+ # ul.add(new HTMLTag("li"))
+ # assert ul.write_to_string == "<ul><li></li></ul>"
+ # returns `self` for fluent programming
+ fun add(child: HTMLTag): HTMLTag
+ do
+ children.add(child)
+ return self
+ end
+
+ # Create a new HTMLTag child and return it
+ #
+ # var ul = new HTMLTag("ul")
+ # ul.open("li").append("1").append("2")
+ # ul.open("li").append("3").append("4")
+ # assert ul.write_to_string == "<ul><li>12</li><li>34</li></ul>"
+ fun open(tag: String): HTMLTag
+ do
+ var res = new HTMLTag(tag)
+ add(res)
+ return res
+ end
# List of children HTML elements
var children: Set[HTMLTag] = new HashSet[HTMLTag]
- # Set text of element
- # var p = new HTMLTag("p")
- # p.text("Hello World!")
- # Text is escaped see: standard::String::html_escape
+ # Clear all child and set the text of element
+ # var p = new HTMLTag("p")
+ # p.text("Hello World!")
+ # assert p.write_to_string == "<p>Hello World!</p>"
+ # Text is escaped see: `standard::String::html_escape`
fun text(txt: String): HTMLTag do
- content.clear
- content.append(txt)
+
+ children.clear
+ append(txt)
return self
end
- private var content = new Buffer
# Append text to element
- # var p = new HTMLTag("p")
- # p.append("Hello").append("<br/>").append("World!")
+ # var p = new HTMLTag("p")
+ # p.append("Hello")
+ # p.add(new HTMLTag("br"))
+ # p.append("World!")
+ # assert p.write_to_string == "<p>Hello<br/>World!</p>"
# Text is escaped see: standard::String::html_escape
fun append(txt: String): HTMLTag do
- content.append(txt)
+ add(new HTMLRaw(txt.html_escape))
+ return self
+ end
+
+ # Append raw HTML to element
+ #
+ # var p = new HTMLTag("p")
+ # p.append("Hello")
+ # p.add_raw_html("<bla/>foo")
+ # assert p.write_to_string == "<p>Hello<bla/>foo</p>"
+ #
+ # Note: the HTML in insered as it, no verification is done.
+ fun add_raw_html(txt: String): HTMLTag do
+ add(new HTMLRaw(txt))
return self
end
- # Render the element as HTML string
- fun html: String do
- var attrs = render_attrs
- var content = render_content
- var str = new Buffer
- str.append("<{tag}")
- str.append(attrs)
- if tag != "script" and content.is_empty then
- str.append("/>")
+ redef fun write_to(stream) do
+ var res = new Array[String]
+ render_in(res)
+ for r in res do
+ stream.write(r)
+ end
+ end
+
+ # In order to avoid recursive concatenation,
+ # this function collects in `res` all the small fragments of `String`
+ private fun render_in(res: Sequence[String])
+ do
+ res.add "<"
+ res.add tag
+ render_attrs_in(res)
+ if is_void and children.is_empty then
+ res.add "/>"
else
- str.append(">")
- str.append(content)
- str.append("</{tag}>")
+ res.add ">"
+ for child in children do child.render_in(res)
+ res.add "</"
+ res.add tag
+ res.add ">"
end
- return str.to_s
end
- private fun render_attrs: String do
- var cls = render_classes
- var css = render_css
- var str = new Buffer
- if not cls.is_empty then
- str.append(" ")
- str.append(render_classes)
+ private fun render_attrs_in(res: Sequence[String]) do
+ if attrs.has_key("class") or not classes.is_empty then
+ res.add " class=\""
+ for cls in classes do
+ res.add cls.html_escape
+ res.add " "
+ end
+ if attrs.has_key("class") then
+ res.add attrs["class"].html_escape
+ res.add " "
+ end
+ if res.last == " " then res.pop
+ res.add "\""
end
- if not css.is_empty then
- str.append(" ")
- str.append(render_css)
+
+ if attrs.has_key("style") or not css_props.is_empty then
+ res.add " style=\""
+ for k, v in css_props do
+ res.add k.html_escape
+ res.add ": "
+ res.add v.html_escape
+ res.add "; "
+ end
+ if attrs.has_key("style") then
+ res.add(attrs["style"].html_escape)
+ end
+ if res.last == "; " then res.pop
+ res.add "\""
end
- if not attrs.is_empty then str.append(" ")
- var count = 0
+
+ if attrs.is_empty then return
+
for key, value in attrs do
if key == "class" or key == "style" then continue
- str.append("{key}=\"{value}\"")
- if count < attrs.length - 1 then
- str.append(" ")
- end
- count += 1
- end
- return str.to_s
- end
-
- private fun render_css: String do
- if not attrs.has_key("style") and css_props.is_empty then return ""
- var css = new Buffer
- css.append("style=\"")
- if attrs.has_key("style") then css.append("{attrs["style"]}; ")
- css.append(css_props.join("; ", ": "))
- css.append("\"")
- return css.to_s
- end
-
- private fun render_classes: String do
- if not attrs.has_key("class") and classes.is_empty then return ""
- var cls = new Buffer
- cls.append("class=\"")
- if attrs.has_key("class") then cls.append("{attrs["class"]} ")
- cls.append(classes.join(" "))
- cls.append("\"")
- return cls.to_s
- end
-
- private fun render_content: String do
- var str = new Buffer
- str.append(content.to_s.html_escape)
- for child in children do
- str.append(child.html)
+ res.add " "
+ res.add key.html_escape
+ res.add "=\""
+ res.add value.html_escape
+ res.add "\""
end
- return str.to_s
end
end
private class HTMLRaw
super HTMLTag
- init(content: String) do self.content.append(content)
- redef fun html do return content.to_s
+ private var content: String
+ init(content: String) do self.content = content
+ redef fun render_in(res) do res.add content
end