All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.jknack.handlebars.helper.StringHelpers Maven / Gradle / Ivy

There is a newer version: 2024.11.18751.20241128T090041Z-241100
Show newest version
/**
 * Copyright (c) 2012-2015 Edgar Espina
 *
 * This file is part of Handlebars.java.
 *
 * 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 com.github.jknack.handlebars.helper;

import com.github.jknack.handlebars.Handlebars;
import com.github.jknack.handlebars.Helper;
import com.github.jknack.handlebars.Options;
import org.apache.commons.lang3.LocaleUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.WordUtils;

import java.io.IOException;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.time.temporal.TemporalAccessor;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static org.apache.commons.lang3.Validate.isTrue;
import static org.apache.commons.lang3.Validate.notNull;
import static org.apache.commons.lang3.Validate.validIndex;

/**
 * Commons string function helpers.
 *
 * @author edgar.espina
 * @since 0.2.2
 */
public enum StringHelpers implements Helper {

  /**
   * Capitalizes the first character of the value.
   * For example:
   *
   * 
   * {{capitalizeFirst value}}
   * 
* * If value is "handlebars.java", the output will be "Handlebars.java". */ capitalizeFirst { @Override protected CharSequence safeApply(final Object value, final Options options) { return StringUtils.capitalize(value.toString()); } }, /** * Centers the value in a field of a given width. * For example: * *
   * {{center value size=19 [pad="char"] }}
   * 
* * If value is "Handlebars.java", the output will be " Handlebars.java ". */ center { @Override protected CharSequence safeApply(final Object value, final Options options) { Integer size = options.hash("size"); notNull(size, "found 'null', expected 'size'"); String pad = options.hash("pad", " "); return StringUtils.center(value.toString(), size, pad); } }, /** * Removes all values of arg from the given string. * For example: * *
   * {{cut value [" "]}}
   * 
* * If value is "String with spaces", the output will be "Stringwithspaces". */ cut { @Override public Object apply(final Object context, final Options options) throws IOException { return safeApply(context, options); } @Override protected CharSequence safeApply(final Object value, final Options options) { if (options.isFalsy(value)) { return ""; } String strip = options.param(0, " "); return value.toString().replace(strip, ""); } }, /** * If value evaluates to False, uses the given default. Otherwise, uses the * value. * For example: * *
   * {{defaultIfEmpty value ["nothing"] }}
   * If value is "" (the empty string), the output will be nothing.
   * 
*/ defaultIfEmpty { @Override public Object apply(final Object value, final Options options) throws IOException { if (Handlebars.Utils.isEmpty(value)) { return options.param(0, ""); } return String.valueOf(value); } @Override protected CharSequence safeApply(final Object context, final Options options) { // Ignored return null; } }, /** * Joins an array, iterator or an iterable with a string. * For example: * *
   * {{join value " // " [prefix=""] [suffix=""]}}
   * 
* *

* If value is the list ['a', 'b', 'c'], the output will be the string "a // b // c". *

* Or: * *
   * {{join "a" "b" "c" " // " [prefix=""] [suffix=""]}}
   * Join the "a", "b", "c", the output will be the string "a // b // c".
   * 
*/ join { @Override public Object apply(final Object context, final Options options) { if (options.isFalsy(context)) { return ""; } return safeApply(context, options); } @SuppressWarnings("rawtypes") @Override protected CharSequence safeApply(final Object context, final Options options) { int separatorIdx = options.params.length - 1; Object separator = options.param(separatorIdx, null); notNull(separator, "found 'null', expected 'separator' at param[%s]", separatorIdx); isTrue(separator instanceof String, "found '%s', expected 'separator' at param[%s]", separator, separatorIdx); String prefix = options.hash("prefix", ""); String suffix = options.hash("suffix", ""); if (context instanceof Iterable) { return prefix + StringUtils.join((Iterable) context, (String) separator) + suffix; } if (context instanceof Iterator) { return prefix + StringUtils.join((Iterator) context, (String) separator) + suffix; } if (context.getClass().isArray()) { return prefix + StringUtils.join((Object[]) context, (String) separator) + suffix; } // join everything as single values Object[] values = new Object[options.params.length]; System.arraycopy(options.params, 0, values, 1, separatorIdx); values[0] = context; return prefix + StringUtils.join(values, (String) separator) + suffix; } }, /** * Left-aligns the value in a field of a given width. * Argument: field size * For example: * *
   * {{ljust value 20 [pad=" "] }}
   * 
* * If value is Handlebars.java, the output will be "Handlebars.java ". */ ljust { @Override protected CharSequence safeApply(final Object value, final Options options) { Integer size = options.hash("size"); notNull(size, "found 'null', expected 'size'"); String pad = options.hash("pad", " "); return StringUtils.rightPad(value.toString(), size, pad); } }, /** * Right-aligns the value in a field of a given width. * Argument: field size * For example: * *
   * {{rjust value 20 [pad=" "] }}
   * 
* * If value is Handlebars.java, the output will be " Handlebars.java". */ rjust { @Override protected CharSequence safeApply(final Object value, final Options options) { Integer size = options.hash("size"); notNull(size, "found 'null', expected 'size'"); String pad = options.hash("pad", " "); return StringUtils.leftPad(value.toString(), size, pad); } }, /** * Returns a new CharSequence that is a subsequence of this sequence. * The subsequence starts with the char value at the specified index and * ends with the char value at index end - 1 * Argument: start offset * end offset * For example: * *
   * {{substring value 11 }}
   * 
* * If value is Handlebars.java, the output will be "java". * * or * *
   * {{substring value 0 10 }}
   * 
* * If value is Handlebars.java, the output will be "Handlebars". */ substring { @Override protected CharSequence safeApply(final Object value, final Options options) { validIndex(options.params, 0, "Required start offset: "); String str = value.toString(); Integer start = options.param(0); Integer end = options.param(1, str.length()); return str.subSequence(start, end); } }, /** * Converts a string into all lowercase. * For example: * *
   * {{lower value}}
   * 
* * If value is 'Still MAD At Yoko', the output will be 'still mad at yoko'. */ lower { @Override protected CharSequence safeApply(final Object value, final Options options) { return value.toString().toLowerCase(); } }, /** * Converts a string into all uppercase. * For example: * *
   * {{upper value}}
   * 
* * If value is 'Hello', the output will be 'HELLO'. */ upper { @Override protected CharSequence safeApply(final Object value, final Options options) { return value.toString().toUpperCase(); } }, /** * Converts to lowercase, removes non-word characters (alphanumerics and * underscores) and converts spaces to hyphens. Also strips leading and * trailing whitespace. * For example: * *
   * {{slugify value}}
   * 
* * If value is "Joel is a slug", the output will be "joel-is-a-slug". */ slugify { @Override protected CharSequence safeApply(final Object context, final Options options) { String value = StringUtils.strip(context.toString()); StringBuilder buffer = new StringBuilder(value.length()); for (int i = 0; i < value.length(); i++) { char ch = value.charAt(i); if (Character.isLetter(ch)) { buffer.append(Character.toLowerCase(ch)); } if (Character.isWhitespace(ch)) { buffer.append('-'); } } return buffer.toString(); } }, /** * Formats the variable according to the argument, a string formatting * specifier. * For example: * *
   * {{stringFormat string param0 param1 ... paramN}}
   * 
* * If value is "Hello %s" "handlebars.java", the output will be * "Hello handlebars.java". * * @see String#format(String, Object...) */ stringFormat { @Override protected CharSequence safeApply(final Object format, final Options options) { return String.format(format.toString(), options.params); } }, /** * Strips all [X]HTML tags. * For example: * *
   * {{stripTags value}}
   * 
*/ stripTags { /** * The HTML tag pattern. */ private final Pattern pattern = Pattern .compile("\\<[^>]*>", Pattern.DOTALL); @Override protected CharSequence safeApply(final Object value, final Options options) { Matcher matcher = pattern.matcher(value.toString()); return matcher.replaceAll(""); } }, /** * Capitalizes all the whitespace separated words in a String. * For example: * *
   * {{ capitalize value [fully=false]}}
   * 
* * If value is "my first post", the output will be "My First Post". */ capitalize { @Override protected CharSequence safeApply(final Object value, final Options options) { Boolean fully = options.hash("fully", false); return fully ? WordUtils.capitalizeFully(value.toString()) : WordUtils.capitalize(value.toString()); } }, /** * Truncates a string if it is longer than the specified number of characters. * Truncated strings will end with a translatable ellipsis sequence ("..."). * Argument: Number of characters to truncate to * For example: * *
   * {{abbreviate value 13 }}
   * 
* * If value is "Handlebars rocks", the output will be "Handlebars...". */ abbreviate { @Override protected CharSequence safeApply(final Object value, final Options options) { Integer width = options.param(0, null); notNull(width, "found 'null', expected 'width'"); return StringUtils.abbreviate(value.toString(), width); } }, /** * Wraps words at specified line length. * Argument: number of characters at which to wrap the text * For example: * *
   * {{ wordWrap value 5 }}
   * 
* * If value is Joel is a slug, the output would be: * *
   * Joel
   * is a
   * slug
   * 
*/ wordWrap { @Override protected CharSequence safeApply(final Object value, final Options options) { Integer length = options.param(0, null); notNull(length, "found 'null', expected 'length'"); return WordUtils.wrap(value.toString(), length); } }, /** * Replaces each substring of this string that matches the literal target * sequence with the specified literal replacement sequence. * For example: * *
   * {{ replace value "..." "rocks" }}
   * 
* * If value is "Handlebars ...", the output will be "Handlebars rocks". * */ replace { @Override public CharSequence safeApply(final Object value, final Options options) { String target = options.param(0, null); String replacement = options.param(1, null); return value.toString().replace(target, replacement); } }, /** * Maps values for true, false and (optionally) null, to the strings "yes", * "no", "maybe". * For example: * *
   * {{yesno value [yes="yes"] [no="no"] maybe=["maybe"] }}
   * 
*/ yesno { @Override public Object apply(final Object value, final Options options) throws IOException { if (value == null) { return options.hash("maybe", "maybe"); } isTrue(value instanceof Boolean, "found '%s', expected 'boolean'", value); if (Boolean.TRUE.equals(value)) { return options.hash("yes", "yes"); } return options.hash("no", "no"); } @Override protected CharSequence safeApply(final Object context, final Options options) { return null; } }, /** *

* Usage: *

* *
   *    {{dateFormat date ["format"] [format="format"][locale="locale"][tz=timeZone|timeZoneId]
   *        [time="format"]}}
   * 
* * Format parameters is one of: *
    *
  • "full": full date format. For example: Tuesday, June 19, 2012
  • *
  • "long": long date format. For example: June 19, 2012
  • *
  • "medium": medium date format. For example: Jun 19, 2012
  • *
  • "short": short date format. For example: 6/19/12
  • *
  • "pattern": a {@link java.time.format.DateTimeFormatter} pattern.
  • *
* Otherwise, the default formatter will be used. * The format option can be specified as a parameter or hash (a.k.a named parameter). * *
    *
  • * The "locale" parameter can be use to select a locale, e.g. "de" or "en_GB". * It defaults to the system locale. *
  • *
  • * The "tz" parameter is the time zone to use, e.g. "Europe/Berlin" or "GMT-8:00". * It defaults to the system time zone. *
  • *
  • * The "time" parameter specifies the format of the time part, it can be "full", "long", * "medium" or "short". * If you do not specify it only the date part will appear in the output string. *
  • *
*/ dateFormat { /** * The default format styles. */ private final Map formatStyles = new HashMap(4) { { put("full", FormatStyle.FULL); put("long", FormatStyle.LONG); put("medium", FormatStyle.MEDIUM); put("short", FormatStyle.SHORT); } }; private TemporalAccessor toTemporalAccessor(final Object value) { if (value instanceof TemporalAccessor) { return (TemporalAccessor) value; } else if (value instanceof Date) { return ((Date) value).toInstant(); } else { String className = null; if (value != null) { className = value.getClass().getSimpleName(); } throw new IllegalArgumentException(String.format( "found instance of %s with value '%s', but expected instance of TemporalAccessor", className, value)); } } @Override protected CharSequence safeApply(final Object value, final Options options) { TemporalAccessor date = toTemporalAccessor(value); String pattern = options.param(0, options.hash("format", "medium")); FormatStyle dateStyle = formatStyles.get(pattern); FormatStyle timeStyle = formatStyles.get(options.hash("time")); DateTimeFormatter formatter; if (dateStyle == null) { formatter = DateTimeFormatter.ofPattern(pattern); } else { if (timeStyle != null) { formatter = DateTimeFormatter.ofLocalizedDateTime(dateStyle, timeStyle); } else { formatter = DateTimeFormatter.ofLocalizedDate(dateStyle); } } // configure locale String localeStr = options.param(1, options.hash("locale")); Locale locale; if (localeStr != null && localeStr.length() > 0) { locale = LocaleUtils.toLocale(localeStr); } else { locale = Locale.getDefault(); } formatter = formatter.withLocale(locale); // configure timezone Object tz = options.hash("tz"); if (tz != null) { ZoneId zoneId; if (tz instanceof ZoneId) { zoneId = (ZoneId) tz; } else if (tz instanceof TimeZone) { zoneId = ((TimeZone) tz).toZoneId(); } else { zoneId = TimeZone.getTimeZone(tz.toString()).toZoneId(); } formatter = formatter.withZone(zoneId); } else { formatter = formatter.withZone(ZoneId.systemDefault()); } return formatter.format(date); } }, /** *

* Usage: *

* *
   *    {{numberFormat number ["format"] [locale=default]}}
   * 
* * Format parameters is one of: *
    *
  • "integer": the integer number format
  • *
  • "percent": the percent number format
  • *
  • "currency": the decimal number format
  • *
  • "pattern": a decimal pattern.
  • *
* Otherwise, the default formatter will be used. * *

* More options: *

*
    *
  • groupingUsed: Set whether or not grouping will be used in this format.
  • *
  • maximumFractionDigits: Sets the maximum number of digits allowed in the fraction portion of * a number.
  • *
  • maximumIntegerDigits: Sets the maximum number of digits allowed in the integer portion of a * number
  • *
  • minimumFractionDigits: Sets the minimum number of digits allowed in the fraction portion of * a number
  • *
  • minimumIntegerDigits: Sets the minimum number of digits allowed in the integer portion of a * number.
  • *
  • parseIntegerOnly: Sets whether or not numbers should be parsed as integers only.
  • *
  • roundingMode: Sets the {@link java.math.RoundingMode} used in this NumberFormat.
  • *
* * @see NumberFormat * @see DecimalFormat */ numberFormat { @Override public Object apply(final Object context, final Options options) throws IOException { if (context instanceof Number) { return safeApply(context, options); } Object param = options.param(0, null); return param == null ? null : param.toString(); } @Override protected CharSequence safeApply(final Object value, final Options options) { isTrue(value instanceof Number, "found '%s', expected 'number'", value); Number number = (Number) value; final NumberFormat numberFormat = build(options); Boolean groupingUsed = options.hash("groupingUsed"); if (groupingUsed != null) { numberFormat.setGroupingUsed(groupingUsed); } Integer maximumFractionDigits = options.hash("maximumFractionDigits"); if (maximumFractionDigits != null) { numberFormat.setMaximumFractionDigits(maximumFractionDigits); } Integer maximumIntegerDigits = options.hash("maximumIntegerDigits"); if (maximumIntegerDigits != null) { numberFormat.setMaximumIntegerDigits(maximumIntegerDigits); } Integer minimumFractionDigits = options.hash("minimumFractionDigits"); if (minimumFractionDigits != null) { numberFormat.setMinimumFractionDigits(minimumFractionDigits); } Integer minimumIntegerDigits = options.hash("minimumIntegerDigits"); if (minimumIntegerDigits != null) { numberFormat.setMinimumIntegerDigits(minimumIntegerDigits); } Boolean parseIntegerOnly = options.hash("parseIntegerOnly"); if (parseIntegerOnly != null) { numberFormat.setParseIntegerOnly(parseIntegerOnly); } String roundingMode = options.hash("roundingMode"); if (roundingMode != null) { numberFormat.setRoundingMode(RoundingMode.valueOf(roundingMode.toUpperCase().trim())); } return numberFormat.format(number); } /** * Build a number format from options. * * @param options The helper options. * @return The number format to use. */ private NumberFormat build(final Options options) { if (options.params.length == 0) { return NumberStyle.DEFAULT.numberFormat(Locale.getDefault()); } isTrue(options.params[0] instanceof String, "found '%s', expected 'string'", options.params[0]); String format = options.param(0); String localeStr = options.param(1, Locale.getDefault().toString()); Locale locale = LocaleUtils.toLocale(localeStr); try { NumberStyle style = NumberStyle.valueOf(format.toUpperCase().trim()); return style.numberFormat(locale); } catch (ArrayIndexOutOfBoundsException ex) { return NumberStyle.DEFAULT.numberFormat(locale); } catch (IllegalArgumentException ex) { return new DecimalFormat(format, new DecimalFormatSymbols(locale)); } } }, /** *

* Usage: *

* *
   *    {{now ["format"] [tz=timeZone|timeZoneId]}}
   * 
* * Format parameters is one of: *
    *
  • "full": full date format. For example: Tuesday, June 19, 2012
  • *
  • "long": long date format. For example: June 19, 2012
  • *
  • "medium": medium date format. For example: Jun 19, 2012
  • *
  • "short": short date format. For example: 6/19/12
  • *
  • "pattern": a date pattern.
  • *
* Otherwise, the default formatter will be used. */ now { @Override protected CharSequence safeApply(final Object value, final Options options) { return StringHelpers.dateFormat.safeApply(new Date(), options); } }; @Override public Object apply(final Object context, final Options options) throws IOException { if (options.isFalsy(context)) { Object param = options.param(0, null); return param == null ? null : param.toString(); } return safeApply(context, options); } /** * Apply the helper to the context. * * @param context The context object (param=0). * @param options The options object. * @return A string result. */ protected abstract CharSequence safeApply(Object context, Options options); /** * Register the helper in a handlebars instance. * * @param handlebars A handlebars object. Required. */ public void registerHelper(final Handlebars handlebars) { notNull(handlebars, "The handlebars is required."); handlebars.registerHelper(name(), this); } /** * Register all the text helpers. * * @param handlebars The helper's owner. Required. */ public static void register(final Handlebars handlebars) { notNull(handlebars, "A handlebars object is required."); StringHelpers[] helpers = values(); for (StringHelpers helper : helpers) { helper.registerHelper(handlebars); } } } /** * Number format styles. * * @author edgar.espina * @since 1.0.1 */ enum NumberStyle { /** * The default number format. */ DEFAULT { @Override public NumberFormat numberFormat(final Locale locale) { return NumberFormat.getInstance(locale); } }, /** * The integer number format. */ INTEGER { @Override public NumberFormat numberFormat(final Locale locale) { return NumberFormat.getIntegerInstance(locale); } }, /** * The currency number format. */ CURRENCY { @Override public NumberFormat numberFormat(final Locale locale) { return NumberFormat.getCurrencyInstance(locale); } }, /** * The percent number format. */ PERCENT { @Override public NumberFormat numberFormat(final Locale locale) { return NumberFormat.getPercentInstance(locale); } }; /** * Build a new number format. * * @param locale The locale to use. * @return A new number format. */ public abstract NumberFormat numberFormat(Locale locale); }