NavigationLabel.java

/*
 * Copyright (C) 2000 - 2025 Silverpeas
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * As a special exception to the terms and conditions of version 3.0 of
 * the GPL, you may redistribute this Program in connection with Free/Libre
 * Open Source Software ("FLOSS") applications as described in Silverpeas's
 * FLOSS exception.  You should have received a copy of the text describing
 * the FLOSS exception, and it is also available here:
 * "https://www.silverpeas.org/legal/floss_exception.html"
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.silverpeas.mobile.client.apps.navigation.pages.widgets;

import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;

/** Keeps a leading emoji separate for optional card layouts, preserving the original label. */
public final class NavigationLabel {
  private NavigationLabel() {}

  public static SafeHtml format(String label) {
    String text = label == null ? "" : label;
    int end = leadingEmojiEnd(text);
    SafeHtmlBuilder html = new SafeHtmlBuilder();
    if (end > 0) {
      html.appendHtmlConstant("<span class='homepage-label-icon'>")
          .appendEscaped(text.substring(0, end)).appendHtmlConstant("</span>");
    }
    return html.appendHtmlConstant("<span class='homepage-label-text'>")
        .appendEscaped(text.substring(end)).appendHtmlConstant("</span>").toSafeHtml();
  }

  static int leadingEmojiEnd(String text) {
    if (text.isEmpty()) return 0;
    int first = text.codePointAt(0);
    int end = Character.charCount(first);
    // Keycap emojis must include the enclosing keycap; ordinary numbers remain text.
    if (first == '#' || first == '*' || (first >= '0' && first <= '9')) {
      if (end < text.length() && text.codePointAt(end) == 0xFE0F) end++;
      return end < text.length() && text.codePointAt(end) == 0x20E3 ? end + 1 : 0;
    }
    if (!isEmoji(first)) return 0;
    if (isRegionalIndicator(first) && end < text.length()
        && isRegionalIndicator(text.codePointAt(end))) {
      end += Character.charCount(text.codePointAt(end));
    }
    while (end < text.length()) {
      int next = text.codePointAt(end);
      if (next == 0xFE0F || (next >= 0x1F3FB && next <= 0x1F3FF)
          || (next >= 0xE0020 && next <= 0xE007F)) {
        end += Character.charCount(next);
      } else if (next == 0x200D && end + 1 < text.length()
          && isEmoji(text.codePointAt(end + 1))) {
        end += 1 + Character.charCount(text.codePointAt(end + 1));
      } else {
        break;
      }
    }
    return end;
  }

  private static boolean isEmoji(int codePoint) {
    return (codePoint >= 0x1F000 && codePoint <= 0x1FAFF)
        || (codePoint >= 0x2600 && codePoint <= 0x27BF)
        || (codePoint >= 0x231A && codePoint <= 0x231B)
        || (codePoint >= 0x23E9 && codePoint <= 0x23F3)
        || (codePoint >= 0x23F8 && codePoint <= 0x23FA)
        || (codePoint >= 0x2B05 && codePoint <= 0x2B07)
        || codePoint == 0x2B50 || codePoint == 0x2B55;
  }

  private static boolean isRegionalIndicator(int codePoint) {
    return codePoint >= 0x1F1E6 && codePoint <= 0x1F1FF;
  }
}