Decompose.java

package com.maybeitssquid.safeascii.internal;

import java.text.Normalizer;
import java.util.function.IntFunction;

/**
 * A {@link Chainable} step that normalizes Unicode characters to a specific form.
 *
 * <p>This class uses {@link java.text.Normalizer} to decompose characters. By default, it uses
 * {@link java.text.Normalizer.Form#NFKD} (Compatibility Decomposition), which is useful for
 * converting compatibility characters (like ligatures or wide characters) into their base
 * components before further processing. Only decomposition is supported, not composition.
 *
 * @see java.text.Normalizer
 * @see java.text.Normalizer.Form
 */
public class Decompose extends Chainable {

  /** The lowest codepoint value that is not decomposed, corresponding to NO-BREAK SPACE */
  public static final int LOWEST_COMPOSED_CODEPOINT = 0x00A0;

  private final Normalizer.Form form;

  /**
   * Creates a new Decompose instance with the specified normalization (decomposition) form.
   *
   * @param delegate the next step in the processing chain
   * @param form the {@link Normalizer.Form} to use for character decomposition, which must be NFD
   *     or NFKD.
   */
  public Decompose(final IntFunction<CharSequence> delegate, final Normalizer.Form form) {
    super(delegate);
    this.form =
        switch (form) {
          case NFD, NFKD -> form;
          default -> throw new IllegalArgumentException("Unsupported Normalizer.Form: " + form);
        };
  }

  /**
   * Creates a new Decompose instance using the default {@link Normalizer.Form#NFKD} normalization
   * form.
   *
   * @param delegate the next step in the processing chain
   */
  public Decompose(final IntFunction<CharSequence> delegate) {
    this(delegate, Normalizer.Form.NFKD);
  }

  /**
   * Normalizes a single codepoint.
   *
   * @param codepoint the Unicode codepoint to process
   * @return the normalized string representation of the codepoint
   */
  @Override
  protected CharSequence process(final int codepoint) {
    final CharSequence input = Character.toString(codepoint);
    return Normalizer.isNormalized(input, form) ? input : Normalizer.normalize(input, form);
  }

  /**
   * Applies normalization to the input value.
   *
   * <p>This method includes an optimization to skip normalization for characters below {@link
   * #LOWEST_COMPOSED_CODEPOINT}, because they are invariant under all normalization forms.
   *
   * @param value the input codepoint
   * @return the processed character sequence from the delegate chain
   */
  @Override
  public CharSequence apply(final int value) {
    return value < LOWEST_COMPOSED_CODEPOINT ? delegate(value) : super.apply(value);
  }
}