Last active
March 13, 2024 10:03
-
-
Save nigjo/3a0590c6f5df112e87b04e63e38f21d7 to your computer and use it in GitHub Desktop.
A simple Builder to generate HTML/XML structures. All attributes, text content or child elements are set or added in one "single" command chain.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// Attribution 4.0 International (CC BY 4.0) | |
// https://creativecommons.org/licenses/by/4.0/ | |
// | |
// Original location: https://gist.github.com/nigjo/3a0590c6f5df112e87b04e63e38f21d7 | |
// Last Changed: 2024-03-13 | |
// Author: Jens Hofschröer <github@nigjo.de> | |
// | |
package com.github.gist.nigjo.web; | |
import java.io.IOException; | |
import java.util.ArrayList; | |
import java.util.LinkedHashMap; | |
import java.util.List; | |
import java.util.Map; | |
import java.util.Objects; | |
import java.util.Optional; | |
/** | |
* A simple Builder to generate HTML/XML structures. All attributes, text content or child | |
* elements are set or added in one "single" command chain. | |
* | |
* <pre> | |
* protected Tag generatePage() | |
* { | |
* return new Tag("html") | |
* .attr("lang", "de") | |
* .add(getHead()) | |
* .add(getBody()); | |
* } | |
* | |
* protected Tag getHead() | |
* { | |
* return new Tag("head") | |
* .add(new Tag("meta") | |
* .dropCloseTag(true) | |
* .attr("charset", "UTF-8") | |
* ) | |
* .add(new Tag("meta") | |
* .dropCloseTag(true) | |
* .attr("name", "viewport") | |
* .attr("content", "width=device-width,initial-scale=1") | |
* ) | |
* .add(new Tag("title") | |
* .content(getPageTitle()) | |
* ); | |
* } | |
* </pre> | |
* | |
* @author Jens Hofschröer | |
*/ | |
public class Tag | |
{ | |
private String name; | |
private String content; | |
private List<Tag> children; | |
private Map<String, String> attributes; | |
private boolean dropCloseTag; | |
private boolean plainText; | |
private boolean selfClosedIfEmpty; | |
public static final Tag EMPTY = new PlainText(); | |
private static class PlainText extends Tag | |
{ | |
public PlainText() | |
{ | |
super(null); | |
} | |
} | |
public static Tag text(String content) | |
{ | |
return new PlainText() | |
.content(content); | |
} | |
public Tag(String name) | |
{ | |
this.name = name; | |
} | |
public final Tag add(Tag child) | |
{ | |
if(content != null) | |
{ | |
throw new IllegalArgumentException("no children allowed"); | |
} | |
if(child != null) | |
{ | |
if(children == null) | |
{ | |
children = new ArrayList<>(); | |
} | |
children.add(child); | |
} | |
return this; | |
} | |
public final Tag content(String content) | |
{ | |
if(this.children != null) | |
{ | |
throw new IllegalArgumentException("no content allowed"); | |
} | |
this.content = content; | |
return this; | |
} | |
public final Tag plainContent(String content) | |
{ | |
plainText = true; | |
return content(content); | |
} | |
public final Tag attr(String attr, String value) | |
{ | |
if(attributes == null) | |
{ | |
attributes = new LinkedHashMap<>(); | |
} | |
attributes.put(attr, value); | |
return this; | |
} | |
public final Tag selfClosedIfEmpty() | |
{ | |
this.selfClosedIfEmpty = true; | |
return this; | |
} | |
@Deprecated | |
public final Tag closed(boolean closed) | |
{ | |
if(closed) | |
{ | |
if(content != null || children != null) | |
{ | |
throw new IllegalArgumentException("already content defined"); | |
} | |
} | |
return dropCloseTag(closed); | |
} | |
public final Tag dropCloseTag(boolean dropCloseTag) | |
{ | |
this.dropCloseTag = dropCloseTag; | |
return this; | |
} | |
public final void appendTo(Appendable dest) throws IOException | |
{ | |
if(this instanceof PlainText) | |
{ | |
if(content != null) | |
{ | |
dest.append(content); | |
} | |
return; | |
} | |
dest.append('<') | |
.append(name); | |
if(attributes != null) | |
{ | |
for(Map.Entry<String, String> entry : attributes.entrySet()) | |
{ | |
dest.append(' ') | |
.append(entry.getKey()) | |
.append("=\"") | |
.append(encode(entry.getValue())) | |
.append('\"'); | |
} | |
} | |
if(selfClosedIfEmpty && content == null && children == null) | |
{ | |
dest.append("/>"); | |
} | |
else | |
{ | |
dest.append(">"); | |
if(content != null) | |
{ | |
if(plainText) | |
{ | |
dest.append("<![CDATA[") | |
.append(content) | |
.append("]]>"); | |
} | |
else | |
{ | |
dest.append(encode(content)); | |
} | |
} | |
else if(children != null) | |
{ | |
for(Tag tag : children) | |
{ | |
tag.appendTo(dest); | |
} | |
} | |
if(!dropCloseTag) | |
{ | |
dest.append("</") | |
.append(name) | |
.append(">"); | |
} | |
} | |
} | |
/** | |
* Create a close-Tag corresponding to this Tag. This String is not used in any other | |
* method of this class. Its for the case where the closing Tag is needed independently | |
* from the "opening" Tag. | |
*/ | |
public String createCloseTag() | |
{ | |
return "</" + name + ">"; | |
} | |
@Override | |
public final String toString() | |
{ | |
StringBuilder b = new StringBuilder(); | |
if(this instanceof PlainText) | |
{ | |
return Objects.toString(content, ""); | |
} | |
try | |
{ | |
appendTo(b); | |
} | |
catch(IOException ex) | |
{ | |
// not here | |
} | |
return b.toString(); | |
} | |
private String encode(String content) | |
{ | |
return content | |
.replace("&", "&") | |
.replace("<", "<") | |
.replace(">", ">") | |
.replace("\"", """); | |
} | |
public final String getName() | |
{ | |
return name; | |
} | |
public final Optional<String> getAttribute(String key) | |
{ | |
if(attributes == null) | |
{ | |
return Optional.empty(); | |
} | |
return Optional.ofNullable(attributes.get(key)); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment