Last active
December 3, 2015 13:12
-
-
Save rjeschke/81d3ba635d70b80c3eaf to your computer and use it in GitHub Desktop.
Simple and fast, single-file template system, designed for: create once, render often/concurrently
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
/* | |
* Copyright (C) 2015 René Jeschke <[email protected]> | |
* | |
* Licensed 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. | |
*/ | |
package rjeschke.toolbox; | |
import java.util.ArrayList; | |
import java.util.Arrays; | |
import java.util.Collection; | |
import java.util.HashMap; | |
import java.util.Map; | |
import java.util.Map.Entry; | |
/** | |
* <p> | |
* Placeholders: <code>${key[:default value]}</code> | |
* </p> | |
* <ul> | |
* <li><code>key</code> is case sensitive and gets surrounding whitespace | |
* removed.</li> | |
* <li><code>default value</code> can be anything, does not get changed except | |
* for delimiter escape sequences (see below).</li> | |
* </ul> | |
* <p> | |
* Delimiter escape sequences: | |
* </p> | |
* <ul> | |
* <li><code>$\{</code> gets replaced by <code>${</code> outside of placeholders | |
* and inside default values.</li> | |
* <li><code>\}</code> only gets replaced by <code>}</code> inside default | |
* values.</li> | |
* </ul> | |
* | |
* @author René Jeschke <[email protected]> | |
*/ | |
public class Template | |
{ | |
private final String[] parts; | |
private final String[] defaults; | |
private final int[] keyIndices; | |
private final HashMap<String, Integer> keyMap; | |
private final int length; | |
private Template(final String[] parts, | |
final String[] defaults, | |
final int[] keyIndices, | |
final HashMap<String, Integer> keyMap) | |
{ | |
this.parts = parts; | |
this.defaults = defaults; | |
this.keyIndices = keyIndices; | |
this.keyMap = keyMap; | |
int len = 0; | |
for (int i = 0; i < parts.length; i++) | |
{ | |
len += parts[i].length(); | |
} | |
this.length = len; | |
} | |
public Filler filler() | |
{ | |
return new Filler(this); | |
} | |
public static class Filler | |
{ | |
private final Template template; | |
private final CharSequence[] values; | |
private Filler(final Template template) | |
{ | |
this.template = template; | |
this.values = new CharSequence[template.keyMap.size()]; | |
this.reset(); | |
} | |
public Filler reset() | |
{ | |
System.arraycopy(this.template.defaults, 0, this.values, 0, this.values.length); | |
return this; | |
} | |
public Filler set(final String key, final CharSequence value) | |
{ | |
final Integer idx = this.template.keyMap.get(key); | |
if (idx != null) | |
{ | |
this.values[idx.intValue()] = value; | |
} | |
return this; | |
} | |
public Filler set(final Map<String, ? extends CharSequence> map) | |
{ | |
for (final Entry<String, ? extends CharSequence> e : map.entrySet()) | |
{ | |
this.set(e.getKey(), e.getValue()); | |
} | |
return this; | |
} | |
public int calculateSize() | |
{ | |
int size = this.template.length; | |
for (int i = 0; i < this.template.keyIndices.length; i++) | |
{ | |
final int idx = this.template.keyIndices[i]; | |
if (idx > -1) | |
{ | |
size += this.values[idx].length(); | |
} | |
} | |
return size; | |
} | |
public String process() | |
{ | |
final StringBuilder sb = new StringBuilder(); | |
this.process(sb); | |
return sb.toString(); | |
} | |
public Filler process(final StringBuilder out) | |
{ | |
out.ensureCapacity(this.calculateSize()); | |
for (int i = 0; i < this.template.parts.length; i++) | |
{ | |
out.append(this.template.parts[i]); | |
final int index = this.template.keyIndices[i]; | |
if (index > -1) | |
{ | |
out.append(this.values[index]); | |
} | |
} | |
return this; | |
} | |
public Filler apply(final Collection<Map<String, ? extends CharSequence>> listOfMaps, final StringBuilder out) | |
{ | |
return this.apply(listOfMaps, false, out); | |
} | |
public Filler apply(final Collection<Map<String, ? extends CharSequence>> listOfMaps, final boolean reset, | |
final StringBuilder out) | |
{ | |
for (final Map<String, ? extends CharSequence> map : listOfMaps) | |
{ | |
if (reset) | |
{ | |
this.reset(); | |
} | |
this.set(map); | |
this.process(out); | |
} | |
return this; | |
} | |
@Override | |
public String toString() | |
{ | |
return this.process(); | |
} | |
} | |
public static Template create(final String input) | |
{ | |
final ArrayList<String> parts = new ArrayList<String>(); | |
final HashMap<String, Integer> keys = new HashMap<String, Integer>(); | |
final IntArrayBuilder inserts = new IntArrayBuilder(32); | |
final ArrayList<String> defaults = new ArrayList<String>(); | |
final StringBuilder sb = new StringBuilder(); | |
for (int i = 0; i < input.length(); i++) | |
{ | |
final char current = input.charAt(i); | |
final char next = i + 1 < input.length() ? input.charAt(i + 1) : '\0'; | |
final char next2 = i + 2 < input.length() ? input.charAt(i + 2) : '\0'; | |
if (current == '$' && next == '\\' && next2 == '{') | |
{ | |
sb.append("${"); | |
i += 2; | |
} | |
else if (current == '$' && next == '{') | |
{ | |
int n = i + 2; | |
boolean inDefault = false; | |
final StringBuilder key = new StringBuilder(); | |
final StringBuilder def = new StringBuilder(); | |
while (n < input.length()) | |
{ | |
final char c0 = input.charAt(n); | |
final char c1 = n + 1 < input.length() ? input.charAt(n + 1) : '\0'; | |
if (!inDefault) | |
{ | |
if (c0 == '}') | |
{ | |
break; | |
} | |
else if (c0 == ':') | |
{ | |
inDefault = true; | |
} | |
else | |
{ | |
key.append(c0); | |
} | |
} | |
else | |
{ | |
if (c0 == '\\' && c1 == '}') | |
{ | |
def.append('}'); | |
n++; | |
} | |
else if (c0 == '}') | |
{ | |
break; | |
} | |
else | |
{ | |
def.append(c0); | |
} | |
} | |
n++; | |
} | |
i = n; | |
parts.add(sb.toString()); | |
sb.setLength(0); | |
final String sKey = key.toString().trim(); | |
final String sDef = def.toString(); | |
if (!keys.containsKey(sKey)) | |
{ | |
keys.put(sKey, Integer.valueOf(keys.size())); | |
defaults.add(""); | |
} | |
final int kIdx = keys.get(sKey).intValue(); | |
inserts.add(kIdx); | |
if (sDef.length() > 0) | |
{ | |
defaults.set(kIdx, sDef); | |
} | |
} | |
else | |
{ | |
sb.append(current); | |
} | |
} | |
parts.add(sb.toString()); | |
inserts.add(-1); | |
return new Template( | |
parts.toArray(new String[parts.size()]), | |
defaults.toArray(new String[defaults.size()]), | |
inserts.toArray(), | |
keys); | |
} | |
private final static class IntArrayBuilder | |
{ | |
private int[] array; | |
private int size; | |
public IntArrayBuilder(final int initialSize) | |
{ | |
this.array = new int[Math.max(0, initialSize)]; | |
} | |
public void add(final int x) | |
{ | |
if (this.size == this.array.length) | |
{ | |
this.resize(); | |
} | |
this.array[this.size++] = x; | |
} | |
public int[] toArray() | |
{ | |
return Arrays.copyOf(this.array, this.size); | |
} | |
private static int guardedDouble(final int x) | |
{ | |
final int y = Math.max(1, x) << 1; | |
return y <= 0 ? Integer.MAX_VALUE : y; | |
} | |
private void resize() | |
{ | |
final int newSize = guardedDouble(this.array.length); | |
if (this.array.length != newSize) | |
{ | |
this.array = Arrays.copyOf(this.array, newSize); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment