StringDecorator.java

package org.ferris.tweial.console.lang;

import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.ferris.tweial.console.util.ArrayTools;

/**
 *
 * @author Michael Remijan mjremijan@yahoo.com @mjremijan
 */
public class StringDecorator {

    @FunctionalInterface
    public static interface Decorator {

        /**
         * The string parameter is decorated and returned
         *
         * @param decorateMe The string that's to be decorated
         *
         * @return The decorated string
         */
        public String decorate(String decorateMe);
    }

    protected static class Decorating {

        int min, max;
        Decorator decorator;
        String text;

        public Decorating(int min, int max, Decorator decorator) {
            this.min = min;
            this.max = max;
            this.decorator = decorator;
            this.text = "";
        }

        public Decorating setString(String s) {
            text = s;
            return this;
        }

        @Override
        public String toString() {
            return this.text;
        }

        public String toStringDecorated() {
            return decorator.decorate(this.text);
        }

        public boolean isOverlapping(Decorating a) {
            return Math.max(this.min, a.min) <= Math.min(this.max, a.max);
        }
    }

    private char[] codePointCharacters;
    private Map<Integer, Decorating> decorators;
    private List<UnicodeCharacter> unicodeCharacters;

    public StringDecorator(String text) {
        codePointCharacters
            = new char[]{};

        unicodeCharacters
            = new LinkedList<>();

        decorators
            = new HashMap<>();

        for (int offset = 0; offset < text.length();) {
            // Get the codepoint at the offset
            UnicodeCharacter unicodeCharacter
                = new UnicodeCharacter(text.codePointAt(offset));

            // Get utf-16 characters
            char[] chars = unicodeCharacter.chars();

            // Change the offset appropriately based on codepoint
            offset += unicodeCharacter.charCount();

            // Store
            codePointCharacters = ArrayTools.concat(codePointCharacters, chars);
            unicodeCharacters.add(unicodeCharacter);
        }
    }

    public List<UnicodeCharacter> getUnicodeCharacters() {
        return unicodeCharacters;
    }

    public void decorate(int startInclusive, int endInclusive, Decorator decorator) {
        // Basic index out of bounds error checking
        if (startInclusive < 0) {
            throw new ArrayIndexOutOfBoundsException(
                String.format(
                    "Value for startInclusive [%d] is out of range",
                    startInclusive
                ));
        }
        if (endInclusive >= codePointCharacters.length) {
            throw new ArrayIndexOutOfBoundsException(
                String.format(
                    "Value for endInclusive [%d] is out of range [%d] ",
                    endInclusive,
                    codePointCharacters.length
                ));
        }
        if (endInclusive < startInclusive) {
            throw new ArrayIndexOutOfBoundsException(
                String.format(
                    "Value for index range not valid: startInclusive [%d], endInclusive [%d]",
                    startInclusive, endInclusive
                ));
        }

        // Holder for decorator information
        Decorating holder = new Decorating(startInclusive, endInclusive, decorator);

        // Does this range overlap with any other range?
        decorators.values().forEach(a -> {
            if (a.isOverlapping(holder)) {
                throw new RuntimeException(
                    String.format(
                        "Decorator range [%d,%d] overlaps with existing range [%d,%d]",
                        holder.min, holder.max,
                        a.min, a.max
                    )
                );
            }
        });

        // No overlap, so build string to store in the holder.
        holder.setString(
            new String(Arrays.copyOfRange(codePointCharacters, holder.min, holder.max + 1))
        );

        // Store the holder
        decorators.put(holder.min, holder);
    }

    public void decorate(String src, Decorator decorator) {
        StringDecorator param = new StringDecorator(src);
        int i = 0;
        while ((i = ArrayTools.indexOfSubArray(this.codePointCharacters, param.codePointCharacters, i)) != -1) {
            this.decorate(i, i + (param.codePointCharacters.length - 1), decorator);
            i = i + param.codePointCharacters.length;
        }
    }

    @Override
    public String toString() {
        if (codePointCharacters.length == 0) {
            return "";
        } else {
            return new String(codePointCharacters);
        }
    }

    public String toStringDecorated() {
        int[] idx = new int[]{0};
        StringBuilder sp = new StringBuilder();

        for (int i = 0; i < codePointCharacters.length; i++) {
            // Does a Decorator exists at this index?
            if (decorators.containsKey(i)) {
                Decorating d = decorators.get(i);
                sp.append(d.toStringDecorated());
                i = d.max;
            } // No Decorator, just add the character
            else {
                sp.append(codePointCharacters[i]);
            }
        }
        return sp.toString();
    }
}