blob: 6458eea0fb6b88605ca3ed7a8f99920fe3b9bbd5 [file] [log] [blame]
package org.apache.velocity.tools.generic;
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.velocity.tools.config.DefaultKey;
/**
* <b>NOTE: This tools is considered "alpha" quality due to lack of testing
* and a generally unpolished API. Feel free to use but expect changes.
* Also, this is not automatically provided via the default tools.xml file.
* </b>
*
* <p>
* A tool to make it easy to generate XML or HTML on the fly. It uses a CSS-type
* syntax with a vaguely jQuery-ish API to help you generate the markup you need.</p>
* <pre>
* Example uses in a template:
* #set( $foospan = $markup.span.id($foo.id).body($foo) )
* $markup.tag('table tr.bar td').body("This is $foospan")
*
* Output:
* &lt;table&gt;
* &lt;tr class="bar"&gt;
* &lt;td&gt;This is &lt;span id="foo1"&gt;my first foo.&lt;/span&gt;&lt;/td&gt;
* &lt;/tr&gt;
* &lt;/table&gt;
*
*
* Example tools.xml config:
* &lt;tools&gt;
* &lt;toolbox scope="application"&gt;
* &lt;tool class="org.apache.velocity.tools.generic.alpha.MarkupTool"/&gt;
* &lt;/toolbox&gt;
* &lt;/tools&gt;
* </pre>
*
* @author Nathan Bubna
* @since VelocityTools 2.0
* @version $Id$
*/
@DefaultKey("mark")
public class MarkupTool extends SafeConfig implements Serializable
{
private static final long serialVersionUID = -777597069616274442L;
public static final String DEFAULT_TAB = " ";
public static final String DEFAULT_DELIMITER = " ";
private String tab = DEFAULT_TAB;
private String delim = DEFAULT_DELIMITER;
/**
* Configuration
* @param tab tab string
*/
public void setTab(String tab)
{
if (isConfigLocked())
{
getLog().error("setTab() failure: configuration is locked");
}
else
{
this.tab = tab;
}
}
public String getTab()
{
return this.tab;
}
public Tag get(String tag)
{
return tag(tag);
}
public Tag tag(String definition)
{
String[] tags = split(definition);
Tag last = null;
for (int i=0; i < tags.length; i++)
{
Tag tag = parse(tags[i]);
if (last != null)
{
last.append(tag);
}
last = tag;
}
return last;
}
protected String[] split(String me)
{
//TODO: fix escaped delimiters
return me.split(delim);
}
private static enum Mode { NAME, ID, CLASS, ATTR }
protected Tag parse(String definition)
{
StringBuilder store = new StringBuilder();
Tag tag = new Tag(this);
Mode mode = Mode.NAME;
for (int i=0; i < definition.length(); i++)
{
char c = definition.charAt(i);
if (c == '#')
{
store = clear(mode, tag, store, true);
mode = Mode.ID;
}
else if (c == '.')
{
store = clear(mode, tag, store, true);
mode = Mode.CLASS;
}
else if (c == '[')
{
store = clear(mode, tag, store, true);
mode = Mode.ATTR;
}
else if (c == ']')
{
store = clear(mode, tag, store, true);
mode = Mode.NAME;
}
else
{
store.append(c);
}
}
clear(mode, tag, store, false);
return tag;
}
private StringBuilder clear(Mode mode, Tag tag, StringBuilder val, boolean emptyStore)
{
if (val.length() > 0)
{
String s = val.toString();
switch (mode)
{
case NAME:
tag.name(s);
break;
case ID:
tag.id(s);
break;
case CLASS:
tag.addClass(s);
break;
case ATTR:
if (s.indexOf('=') > 0)
{
String[] kv = s.split("=");
tag.attr(kv[0], kv[1]);
}
else
{
tag.attr(s, null);
}
break;
}
if (emptyStore)
{
return new StringBuilder();
}
return val;
}
else
{
// already is clean
return val;
}
}
public static class Tag
{
private MarkupTool tool;
private Tag parent;
private Object name;
private Object id;
private List<Object> classes;
private Map<Object,Object> attributes;
private List<Object> children;
public Tag(MarkupTool tool)
{
this.tool = tool;
}
public Tag name(Object name)
{
this.name = name;
return this;
}
public Tag id(Object id)
{
this.id = id;
return this;
}
public Tag addClass(Object c)
{
if (c == null)
{
return null;
}
if (classes == null)
{
classes = new ArrayList<Object>();
}
classes.add(c);
return this;
}
public Tag attr(Object k, Object v)
{
if (k == null)
{
return null;
}
if (attributes == null)
{
attributes = new HashMap<Object,Object>();
}
attributes.put(k, v);
return this;
}
public Tag body(Object o)
{
if (children == null)
{
children = new ArrayList<Object>();
}
else
{
children.clear();
}
children.add(o);
return this;
}
public Tag append(Object o)
{
if (children == null)
{
children = new ArrayList<Object>();
}
children.add(o);
if (o instanceof Tag)
{
((Tag)o).parent(this);
}
return this;
}
public Tag prepend(Object o)
{
if (children == null)
{
children = new ArrayList<Object>();
children.add(o);
}
else
{
children.add(0, o);
}
if (o instanceof Tag)
{
((Tag)o).parent(this);
}
return this;
}
public Tag wrap(String tag)
{
// make new tag
Tag prnt = tool.tag(tag);
// give root of new tag our parent
prnt.root().parent(parent());
// make new tag our parent
parent(prnt);
return this;
}
public Tag orphan()
{
return parent(null);
}
public Tag parent(Tag parent)
{
this.parent = parent;
return this;
}
public Tag parent()
{
return this.parent;
}
public Tag root()
{
if (isOrphan())
{
return this;
}
return this.parent.root();
}
public List<Object> children()
{
return children;
}
public boolean isOrphan()
{
return (parent == null);
}
public boolean isEmpty()
{
return (children == null || children().isEmpty());
}
public boolean matches(Tag tag)
{
if (missed(name, tag.name) ||
missed(id, tag.id) ||
missed(classes, tag.classes))
{
return false;
}
//TODO: match attributes
return true;
}
protected boolean missed(Object target, Object arrow)
{
// no arrow, no miss
if (arrow == null)
{
return false;
}
// otherwise, the arrow must hit the target
return !arrow.equals(target);
}
protected boolean missed(List<Object> targets, List<Object> arrows)
{
// no arrows, no miss
if (arrows == null)
{
return false;
}
// no targets, always miss
if (targets == null)
{
return true;
}
for (Object o : arrows)
{
if (!targets.contains(o))
{
return true;
}
}
return false;
}
/************* rendering methods **************/
/**
* Render tag
* @param indent indentation string
* @param s string builder
*/
protected void render(String indent, StringBuilder s)
{
if (render_start(indent, s))
{
render_body(indent, s);
render_end(indent, s);
}
}
/**
* Start tag rendering
* @param indent indentation string
* @param s string builder
* @return whether a closing tag is needed
*/
protected boolean render_start(String indent, StringBuilder s)
{
if (indent != null)
{
s.append(indent);
}
s.append('<');
render_name(s);
render_id(s);
render_classes(s);
render_attributes(s);
if (isEmpty())
{
s.append("/>");
return false;
}
else
{
s.append('>');
return true;
}
}
/**
* Render tag name
* @param s string builder
*/
protected void render_name(StringBuilder s)
{
s.append(name == null ? "div" : name);
}
/**
* render tag id
* @param s string builder
*/
protected void render_id(StringBuilder s)
{
if (id != null)
{
s.append(" id=\"").append(id).append('"');
}
}
/**
* render tag classes
* @param s string builder
*/
protected void render_classes(StringBuilder s)
{
if (classes != null)
{
s.append(" class=\"");
for (int i=0; i < classes.size(); i++)
{
s.append(classes.get(i));
if (i + 1 != classes.size())
{
s.append(' ');
}
}
s.append('"');
}
}
/**
* Render tag attributes
* @param s string builder
*/
protected void render_attributes(StringBuilder s)
{
if (attributes != null)
{
for (Map.Entry<Object,Object> entry : attributes.entrySet())
{
s.append(' ').append(entry.getKey()).append("=\"");
if (entry.getValue() != null)
{
s.append(entry.getValue());
}
s.append('"');
}
}
}
/**
* Render tag body
* @param indent indentation string
* @param s string builder
*/
protected void render_body(String indent, StringBuilder s)
{
String kidIndent = indent + tool.getTab();
for (Object o : children)
{
if (o instanceof Tag)
{
((Tag)o).render(kidIndent, s);
}
else
{
s.append(kidIndent);
s.append(o);
}
}
}
/**
* Render tag end
* @param indent indentation string
* @param s string builder
*/
protected void render_end(String indent, StringBuilder s)
{
if (indent != null)
{
s.append(indent);
}
s.append("</").append(name).append('>');
}
/**
* @return string representation
*/
public String toString()
{
StringBuilder s = new StringBuilder();
root().render("\n", s);
return s.toString();
}
}
}