Skip to content

Instantly share code, notes, and snippets.

@nigjo
Last active March 13, 2024 10:03
Show Gist options
  • Save nigjo/3a0590c6f5df112e87b04e63e38f21d7 to your computer and use it in GitHub Desktop.
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.
//
// 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("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;");
}
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