package nl.astraeus.tag import java.util.* /** * Created by rnentjes on 15-9-15. */ fun escape(s: String): String { if ("" == s) { return "" } val out = StringBuilder(Math.max(16, s.length)) for (i in 0..s.length - 1) { val c = s.get(i) if (c.toInt() > 127 || c == '"' || c == '<' || c == '>' || c == '&') { out.append("&#") out.append(c.toInt()) out.append(';') } else { out.append(c) } } return out.toString() } object Attr { val HIDDEN: String = "hidden" val HIGH: String = "high" val HREF: String = "href" val HREFLANG: String = "hreflang" val HTTP_EQUIV: String = "http-equiv" val ICON: String = "icon" val ID: String = "id" val ISMAP: String = "ismap" val ITEMPROP: String = "itemprop" val KEYTYPE: String = "keytype" val KIND: String = "kind" val LABEL: String = "label" val LANG: String = "lang" val LANGUAGE: String = "language" val LIST: String = "list" val LOOP: String = "loop" val LOW: String = "low" val MANIFEST: String = "manifest" val MAX: String = "max" val MAXLENGTH: String = "maxlength" val MEDIA: String = "media" val METHOD: String = "method" val MIN: String = "min" val MULTIPLE: String = "multiple" val NAME: String = "name" val NOVALIDATE: String = "novalidate" val OPEN: String = "open" val OPTIMUM: String = "optimum" val PATTERN: String = "pattern" val PING: String = "ping" val PLACEHOLDER: String = "placeholder" val POSTER: String = "poster" val PRELOAD: String = "preload" val PUBDATE: String = "pubdate" val RADIOGROUP: String = "radiogroup" val READONLY: String = "readonly" val REL: String = "rel" val REQUIRED: String = "required" val REVERSED: String = "reversed" val ROWS: String = "rows" val ROWSPAN: String = "rowspan" val SANDBOX: String = "sandbox" val SPELLCHECK: String = "spellcheck" val SCOPE: String = "scope" val SCOPED: String = "scoped" val SEAMLESS: String = "seamless" val SELECTED: String = "selected" val SHAPE: String = "shape" val SIZE: String = "size" val SIZES: String = "sizes" val SPAN: String = "span" val SRC: String = "src" val SRCDOC: String = "srcdoc" val SRCLANG: String = "srclang" val SRCSET: String = "srcset" val START: String = "start" val STEP: String = "step" val STYLE: String = "style" val SUMMARY: String = "summary" val TABINDEX: String = "tabindex" val TARGET: String = "target" val TITLE: String = "title" val TYPE: String = "type" val USEMAP: String = "usemap" val VALUE: String = "value" val WIDTH: String = "width" val WRAP: String = "wrap" val BORDER: String = "border" val BUFFERED: String = "buffered" val CHALLENGE: String = "challenge" val CHARSET: String = "charset" val CHECKED: String = "checked" val CITE: String = "cite" val CLASS: String = "class" val COLOR: String = "color" val COLS: String = "cols" val COLSPAN: String = "colspan" val CONTENT: String = "content" val CONTENTEDITABLE: String = "contenteditable" val CONTEXTMENU: String = "contextmenu" val CONTROLS: String = "controls" val COORDS: String = "coords" val DATA: String = "data" val DATETIME: String = "datetime" val DEFAULT: String = "default" val DEFER: String = "defer" val DIR: String = "dir" val DIRNAME: String = "dirname" val DISABLED: String = "disabled" val DOWNLOAD: String = "download" val DRAGGABLE: String = "draggable" val DROPZONE: String = "dropzone" val ENCTYPE: String = "enctype" val FOR: String = "for" val FORM: String = "form" val FORMACTION: String = "formaction" val HEADERS: String = "headers" val HEIGHT: String = "height" val ACCEPT: String = "accept" val ACCEPT_CHARSET: String = "accept-charset" val ACCESSKEY: String = "accesskey" val ACTION: String = "action" val ALIGN: String = "align" val ALT: String = "alt" val ASYNC: String = "async" val AUTOCOMPLETE: String = "autocomplete" val AUTOFOCUS: String = "autofocus" val AUTOPLAY: String = "autoplay" val AUTOSAVE: String = "autosave" } class Attribute { var name: String private set private var value: String? = null constructor(name: String, value: String) { this.name = name this.value = escape(value) } constructor(name: String) { this.name = name this.value = null } fun render(): String { if (value == null) { return " " + name } return (" $name=\"$value\"") } override fun toString() = this.render() fun setValue(value: String) { this.value = value } } abstract class Tag protected constructor(protected var tag: String) { protected var attributes: ArrayList<Attribute> = ArrayList() var parent: Tag? = null /** * Sets an attribute on an element * @param name the attribute * * * @param value the attribute value */ fun setAttribute(name: String, value: String?): Boolean { if (value == null) { return attributes.add(Attribute(name)) } for (attribute in attributes) { if (attribute.name == name) { attribute.setValue(value) //update with new value return true } } return attributes.add(Attribute(name, value)) } fun indent(indent: Int): String { var result = StringBuilder() for(i in 1..indent) { result.append(" ") } return result.toString() } open fun render(indent: Int = 0, pretty: Boolean = false): String { var result = StringBuilder() if (pretty) { result.append(indent(indent)) } result.append(renderOpenTag()) result.append(renderCloseTag()) return result.toString() } override fun toString() = this.render() fun renderOpenTag(): String { var tagAttributes = "" for (attribute in attributes) { tagAttributes += attribute.render() } return "<$tag$tagAttributes>" } fun renderCloseTag() = "</$tag>" } class EmptyTag(tagType: String) : Tag(tagType) { /** * Sets a custom attribute * @param attribute the attribute name * * * @param value the attribute value * * * @return itself for easy chaining */ fun attr(attribute: String, value: String?): EmptyTag { setAttribute(attribute, value) return this } /** * Call attr-method based on condition * [.attr] */ fun condAttr(condition: Boolean, attribute: String, value: String?): EmptyTag { return if (condition) attr(attribute, value) else this } override fun render(indent: Int, pretty: Boolean): String { var tagAttributes = "" for (attribute in attributes) { tagAttributes += attribute } if (pretty) { return indent(indent) + "<$tag$tagAttributes/>" } else { return "<$tag$tagAttributes/>" } } override fun toString() = this.render() /** * Methods below this point are convenience methods * that call attr with a predefined attribute. */ //TODO: TEST ? fun isAutoComplete() = attr(Attr.AUTOCOMPLETE, null) fun isAutoFocus() = attr(Attr.AUTOFOCUS, null) fun isHidden() = attr(Attr.HIDDEN, null) fun isRequired() = attr(Attr.REQUIRED, null) fun withAlt(alt: String) = attr(Attr.ALT, alt) fun withAction(action: String) = attr(Attr.ACTION, action) fun withCharset(charset: String) = attr(Attr.CHARSET, charset) fun withClass(className: String) = attr(Attr.CLASS, className) fun withContent(content: String) = attr(Attr.CONTENT, content) fun withHref(href: String) = attr(Attr.HREF, href) fun withId(id: String) = attr(Attr.ID, id) fun withData(dataAttr: String, value: String) = attr(Attr.DATA + "-" + dataAttr, value) fun withMethod(method: String) = attr(Attr.METHOD, method) fun withName(name: String) = attr(Attr.NAME, name) fun withPlaceholder(placeholder: String) = attr(Attr.PLACEHOLDER, placeholder) fun withTarget(target: String) = attr(Attr.TARGET, target) fun withType(type: String) = attr(Attr.TYPE, type) fun withRel(rel: String) = attr(Attr.REL, rel) fun withSrc(src: String) = attr(Attr.SRC, src) fun withValue(value: String) = attr(Attr.VALUE, value) fun withCondAutoComplete(condition: Boolean) = condAttr(condition, Attr.AUTOCOMPLETE, null) fun withCondAutoFocus(condition: Boolean) = condAttr(condition, Attr.AUTOFOCUS, null) fun withCondHidden(condition: Boolean) = condAttr(condition, Attr.HIDDEN, null) fun withCondRequired(condition: Boolean) = condAttr(condition, Attr.REQUIRED, null) fun withCondAlt(condition: Boolean, alt: String) = condAttr(condition, Attr.ALT, alt) fun withCondAction(condition: Boolean, action: String) = condAttr(condition, Attr.ACTION, action) fun withCharset(condition: Boolean, charset: String) = condAttr(condition, Attr.CHARSET, charset) fun withCondClass(condition: Boolean, className: String) = condAttr(condition, Attr.CLASS, className) fun withCondContent(condition: Boolean, content: String) = condAttr(condition, Attr.CONTENT, content) fun withCondHref(condition: Boolean, href: String) = condAttr(condition, Attr.HREF, href) fun withCondId(condition: Boolean, id: String) = condAttr(condition, Attr.ID, id) fun withCondData(condition: Boolean, dataAttr: String, value: String) = condAttr(condition, Attr.DATA + "-" + dataAttr, value) fun withCondMethod(condition: Boolean, method: String) = condAttr(condition, Attr.METHOD, method) fun withCondName(condition: Boolean, name: String) = condAttr(condition, Attr.NAME, name) fun withCondPlaceholder(condition: Boolean, placeholder: String) = condAttr(condition, Attr.PLACEHOLDER, placeholder) fun withCondTarget(condition: Boolean, target: String) = condAttr(condition, Attr.TARGET, target) fun withCondType(condition: Boolean, type: String) = condAttr(condition, Attr.TYPE, type) fun withCondRel(condition: Boolean, rel: String) = condAttr(condition, Attr.REL, rel) fun withCondSrc(condition: Boolean, src: String) = condAttr(condition, Attr.SRC, src) fun withCondValue(condition: Boolean, value: String) = condAttr(condition, Attr.VALUE, value) } class Text(text: String) : Tag(text) { override fun render(indent: Int, pretty: Boolean): String { if (pretty) { return indent(indent) + escape(super.tag) } else { return escape(super.tag) } } } class UnescapedText(text: String) : Tag(text) { override fun render(indent: Int, pretty: Boolean): String { if (pretty ) { return indent(indent) + tag } else { return indent(indent) + tag } } override fun toString() = this.render() } class ContainerTag(tagType: String) : Tag(tagType), Cloneable { var children: MutableList<Tag> = ArrayList() /** * Appends a tag to the end of this element * @param child tag to be appended * * * @return itself for easy chaining */ fun with(child: Tag): ContainerTag { if (this === child) { throw Error("Cannot append a tag to itself.") } child.parent = this children.add(child) return this } operator fun plus(child: Tag): ContainerTag { if (this === child) { throw Error("Cannot append a tag to itself.") } var result = copy() child.parent = this result.children.add(child) return result } operator fun mod(child: Tag): ContainerTag { return with(child) } fun copy(): ContainerTag { var result = ContainerTag(tag) result.children = ArrayList(children) return result; } /** * Call with-method based on condition * [.with] */ fun condWith(condition: Boolean, child: Tag) = if (condition) this.with(child) else this /** * Appends a list of tags to the end of this element * @param children tags to be appended * * * @return itself for easy chaining */ fun with(children: List<Tag>?): ContainerTag { children?.forEach { this.with(it) } return this } /** * Call with-method based on condition * [.with] */ fun condWith(condition: Boolean, children: List<Tag>) = if (condition) this.with(children) else this /** * Appends the tags to the end of this element * @param children tags to be appended * * * @return itself for easy chaining */ fun with(vararg children: Tag): ContainerTag { for (aChildren in children) { with(aChildren) } return this } /** * Call with-method based on condition * [.with] */ fun condWith(condition: Boolean, vararg children: Tag) = if (condition) this.with(*children) else this /** * Appends a text tag to this element * @param text the text to be appended * * * @return itself for easy chaining */ fun withText(text: String) = with(Text(text)) /** * Sets a custom attribute * @param attribute the attribute name * * * @param value the attribute value * * * @return itself for easy chaining */ fun attr(attribute: String, value: String?): ContainerTag { setAttribute(attribute, value) return this } /** * Call attr-method based on condition * [.attr] */ fun condAttr(condition: Boolean, attribute: String, value: String?) = if (condition) attr(attribute, value) else this /** * Render the tag and its children */ override fun render(indent: Int, pretty: Boolean): String { //very slow // return renderOpenTag() + children.stream().map(Tag::render).collect(Collectors.joining()) + renderCloseTag(); //pretty fast // StringBuilder rendered = new StringBuilder(renderOpenTag()); // children.forEach(rendered::append); // rendered.append(renderCloseTag()); // return rendered.toString(); //fastest val rendered = StringBuilder() if (pretty) { rendered.append(indent(indent)) } rendered.append(renderOpenTag()) if (pretty && !children.isEmpty()) { rendered.append("\n") } for (child in children) { rendered.append(child.render(indent + 2, pretty)) if (pretty) { rendered.append("\n") } } if (pretty && !children.isEmpty()) { rendered.append(indent(indent)) } rendered.append(renderCloseTag()) return rendered.toString() } override fun toString(): String { return this.render() } /** * Methods below this point are convenience methods * that call attr with a predefined attribute. */ fun isAutoComplete() = attr(Attr.AUTOCOMPLETE, null) fun isAutoFocus() = attr(Attr.AUTOFOCUS, null) fun isHidden() = attr(Attr.HIDDEN, null) fun isRequired() = attr(Attr.REQUIRED, null) fun withAlt(alt: String) = attr(Attr.ALT, alt) fun withAction(action: String) = attr(Attr.ACTION, action) fun withCharset(charset: String) = attr(Attr.CHARSET, charset) fun withClass(className: String) = attr(Attr.CLASS, className) fun withContent(content: String) = attr(Attr.CONTENT, content) fun withHref(href: String) = attr(Attr.HREF, href) fun withId(id: String) = attr(Attr.ID, id) fun withData(dataAttr: String, value: String) = attr(Attr.DATA + "-" + dataAttr, value) fun withMethod(method: String) = attr(Attr.METHOD, method) fun withName(name: String) = attr(Attr.NAME, name) fun withPlaceholder(placeholder: String) = attr(Attr.PLACEHOLDER, placeholder) fun withTarget(target: String) = attr(Attr.TARGET, target) fun withType(type: String) = attr(Attr.TYPE, type) fun withRel(rel: String) = attr(Attr.REL, rel) fun withSrc(src: String) = attr(Attr.SRC, src) fun withValue(value: String) = attr(Attr.VALUE, value) fun withCondAutoComplete(condition: Boolean) = condAttr(condition, Attr.AUTOCOMPLETE, null) fun withCondAutoFocus(condition: Boolean) = condAttr(condition, Attr.AUTOFOCUS, null) fun withCondHidden(condition: Boolean) = condAttr(condition, Attr.HIDDEN, null) fun withCondRequired(condition: Boolean) = condAttr(condition, Attr.REQUIRED, null) fun withCondAlt(condition: Boolean, alt: String) = condAttr(condition, Attr.ALT, alt) fun withCondAction(condition: Boolean, action: String) = condAttr(condition, Attr.ACTION, action) fun withCharset(condition: Boolean, charset: String) = condAttr(condition, Attr.CHARSET, charset) fun withCondClass(condition: Boolean, className: String) = condAttr(condition, Attr.CLASS, className) fun withCondContent(condition: Boolean, content: String) = condAttr(condition, Attr.CONTENT, content) fun withCondHref(condition: Boolean, href: String) = condAttr(condition, Attr.HREF, href) fun withCondId(condition: Boolean, id: String) = condAttr(condition, Attr.ID, id) fun withCondData(condition: Boolean, dataAttr: String, value: String) = condAttr(condition, Attr.DATA + "-" + dataAttr, value) fun withCondMethod(condition: Boolean, method: String) = condAttr(condition, Attr.METHOD, method) fun withCondName(condition: Boolean, name: String) = condAttr(condition, Attr.NAME, name) fun withCondPlaceholder(condition: Boolean, placeholder: String) = condAttr(condition, Attr.PLACEHOLDER, placeholder) fun withCondTarget(condition: Boolean, target: String) = condAttr(condition, Attr.TARGET, target) fun withCondType(condition: Boolean, type: String) = condAttr(condition, Attr.TYPE, type) fun withCondRel(condition: Boolean, rel: String) = condAttr(condition, Attr.REL, rel) fun withCondSrc(condition: Boolean, src: String) = condAttr(condition, Attr.SRC, src) fun withCondValue(condition: Boolean, value: String) = condAttr(condition, Attr.VALUE, value) } fun tag(tagName: String) = ContainerTag(tagName) fun emptyTag(tagName: String) = EmptyTag(tagName) fun text(text: String) = Text(text) fun unsafeHtml(html: String) = UnescapedText(html) //EmptyTags fun area() = EmptyTag("area") fun base() = EmptyTag("base") fun br() = EmptyTag("br") fun col() = EmptyTag("col") fun document() = EmptyTag("!DOCTYPE html") fun embed() = EmptyTag("embed") fun hr() = EmptyTag("hr") fun img() = EmptyTag("img") fun input() = EmptyTag("input") fun keygen() = EmptyTag("keygen") fun link() = EmptyTag("link") fun meta() = EmptyTag("meta") fun param() = EmptyTag("param") fun source() = EmptyTag("source") fun track() = EmptyTag("track") fun wbr() = EmptyTag("wbr") // ContainerTags fun a() = tag("a") fun a(text: String) = tag("a").withText(text) fun a(text: String, href: String) = tag("a").withText(text).withHref(href) fun abbr() = ContainerTag("abbr") fun address() = ContainerTag("address") fun article() = ContainerTag("article") fun aside() = ContainerTag("aside") fun audio() = ContainerTag("audio") fun b() = ContainerTag("b") fun b(text: String) = ContainerTag("b").withText(text) fun bdi() = ContainerTag("bdi") fun bdi(text: String) = ContainerTag("bdi").withText(text) fun bdo() = ContainerTag("bdo") fun bdo(text: String) = ContainerTag("bdo").withText(text) fun blockquote() = ContainerTag("blockquote") fun blockquote(text: String) = ContainerTag("blockquote").withText(text) fun body() = ContainerTag("body") fun button() = ContainerTag("button") fun button(text: String) = ContainerTag("button").withText(text) fun canvas() = ContainerTag("canvas") fun caption() = ContainerTag("caption") fun caption(text: String) = ContainerTag("caption").withText(text) fun cite() = ContainerTag("cite") fun cite(text: String) = ContainerTag("cite").withText(text) fun code() = ContainerTag("code") fun colgroup() = ContainerTag("colgroup") fun datalist() = ContainerTag("datalist") fun dd() = ContainerTag("dd") fun dd(text: String) = ContainerTag("dd").withText(text) fun del() = ContainerTag("del") fun del(text: String) = ContainerTag("del").withText(text) fun details() = ContainerTag("details") fun dfn() = ContainerTag("dfn") fun dfn(text: String) = ContainerTag("dfn").withText(text) fun dialog() = ContainerTag("dialog") fun dialog(text: String) = ContainerTag("dialog").withText(text) fun div() = ContainerTag("div") fun dl() = ContainerTag("dl") fun dt() = ContainerTag("dt") fun dt(text: String) = ContainerTag("dt").withText(text) fun em() = ContainerTag("em") fun em(text: String) = ContainerTag("em").withText(text) fun fieldset() = ContainerTag("fieldset") fun figcaption() = ContainerTag("figcaption") fun figcaption(text: String) = ContainerTag("figcaption").withText(text) fun figure() = ContainerTag("figure") fun footer() = ContainerTag("footer") fun form() = ContainerTag("form") fun h1() = ContainerTag("h1") fun h1(text: String) = ContainerTag("h1").withText(text) fun h2() = ContainerTag("h2") fun h2(text: String) = ContainerTag("h2").withText(text) fun h3() = ContainerTag("h3") fun h3(text: String) = ContainerTag("h3").withText(text) fun h4() = ContainerTag("h4") fun h4(text: String) = ContainerTag("h4").withText(text) fun h5() = ContainerTag("h5") fun h5(text: String) = ContainerTag("h5").withText(text) fun h6() = ContainerTag("h6") fun h6(text: String) = ContainerTag("h6").withText(text) fun head() = ContainerTag("head") fun header() = ContainerTag("header") fun html() = ContainerTag("html") fun i() = ContainerTag("i") fun i(text: String) = ContainerTag("i").withText(text) fun iframe() = ContainerTag("iframe") fun ins() = ContainerTag("ins") fun ins(text: String) = ContainerTag("ins").withText(text) fun kbd() = ContainerTag("kbd") fun label() = ContainerTag("label") fun label(text: String) = ContainerTag("label").withText(text) fun legend() = ContainerTag("legend") fun legend(text: String) = ContainerTag("legend").withText(text) fun li() = ContainerTag("li") fun li(text: String) = ContainerTag("li").withText(text) fun main() = ContainerTag("main") fun map() = ContainerTag("map") fun mark() = ContainerTag("mark") fun menu() = ContainerTag("menu") fun menuitem() = ContainerTag("menuitem") fun meter() = ContainerTag("meter") fun nav() = ContainerTag("nav") fun noscript() = ContainerTag("noscript") fun object_() = ContainerTag("object") fun ol() = ContainerTag("ol") fun optgroup() = ContainerTag("optgroup") fun option() = ContainerTag("option") fun option(text: String) = ContainerTag("option").withText(text) fun output() = ContainerTag("output") fun p() = ContainerTag("p") fun p(text: String) = ContainerTag("p").withText(text) fun pre() = ContainerTag("pre") fun progress() = ContainerTag("progress") fun q() = ContainerTag("q") fun q(text: String) = ContainerTag("q").withText(text) fun rp() = ContainerTag("rp") fun rt() = ContainerTag("rt") fun ruby() = ContainerTag("ruby") fun s() = ContainerTag("s") fun s(text: String) = ContainerTag("s").withText(text) fun samp() = ContainerTag("samp") fun script() = ContainerTag("script") fun section() = ContainerTag("section") fun select() = ContainerTag("select") fun small() = ContainerTag("small") fun small(text: String) = ContainerTag("small").withText(text) fun span() = ContainerTag("span") fun span(text: String) = ContainerTag("span").withText(text) fun strong() = ContainerTag("strong") fun strong(text: String) = ContainerTag("strong").withText(text) fun style() = ContainerTag("style") fun sub() = ContainerTag("sub") fun sub(text: String) = ContainerTag("sub").withText(text) fun summary() = ContainerTag("summary") fun summary(text: String) = ContainerTag("summary").withText(text) fun sup() = ContainerTag("sup") fun sup(text: String) = ContainerTag("sup").withText(text) fun table() = ContainerTag("table") fun tbody() = ContainerTag("tbody") fun td() = ContainerTag("td") fun td(text: String) = ContainerTag("td").withText(text) fun textarea() = ContainerTag("textarea") fun tfoot() = ContainerTag("tfoot") fun th() = ContainerTag("th") fun th(text: String) = ContainerTag("th").withText(text) fun thead() = ContainerTag("thead") fun time() = ContainerTag("time") fun title() = ContainerTag("title") fun title(text: String) = ContainerTag("title").withText(text) fun tr() = ContainerTag("tr") fun u() = ContainerTag("u") fun u(text: String) = ContainerTag("u").withText(text) fun ul() = ContainerTag("ul") fun var_() = ContainerTag("var") fun videa() = ContainerTag("video")