| /* |
| * Copyright 2010 Google Inc. |
| * |
| * 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.google.gwt.i18n.shared; |
| |
| import com.google.gwt.i18n.client.HasDirection.Direction; |
| import com.google.gwt.safehtml.shared.SafeHtmlUtils; |
| import com.google.gwt.safehtml.shared.annotations.IsSafeHtml; |
| import com.google.gwt.safehtml.shared.annotations.SuppressIsSafeHtmlCastCheck; |
| |
| /** |
| * Base class for {@link BidiFormatter} and {@link SafeHtmlBidiFormatter} that |
| * contains their common implementation. |
| */ |
| public abstract class BidiFormatterBase { |
| |
| /** |
| * Abstract factory class for BidiFormatterBase. |
| * BidiFormatterBase subclasses will usually have a non-abstract inner Factory |
| * class subclassed from this one, and use a static member of that class in |
| * order to prevent the needless creation of objects. For example, see |
| * {@link BidiFormatter}. |
| */ |
| protected abstract static class Factory<T extends BidiFormatterBase> { |
| private T[] instances; |
| |
| @SuppressWarnings("unchecked") |
| public Factory() { |
| instances = (T[]) new BidiFormatterBase[6]; |
| } |
| |
| public abstract T createInstance(Direction contextDir, |
| boolean alwaysSpan); |
| |
| public T getInstance(Direction contextDir, |
| boolean alwaysSpan) { |
| int index = calculateIndex(contextDir, alwaysSpan); |
| T formatter = instances[index]; |
| if (formatter == null) { |
| formatter = createInstance(contextDir, alwaysSpan); |
| instances[index] = formatter; |
| } |
| return formatter; |
| } |
| |
| // Index should be in the range [0, 5]. |
| private int calculateIndex(Direction contextDir, boolean alwaysSpan) { |
| int i = contextDir == Direction.LTR ? 0 : contextDir == Direction.RTL ? 1 |
| : 2; |
| if (alwaysSpan) { |
| i += 3; |
| } |
| return i; |
| } |
| } |
| |
| /** |
| * A container class for direction-related string constants, e.g. Unicode |
| * formatting characters. |
| */ |
| static final class Format { |
| /** |
| * "left" string constant. |
| */ |
| public static final String LEFT = "left"; |
| |
| /** |
| * Unicode "Left-To-Right Embedding" (LRE) character. |
| */ |
| public static final char LRE = '\u202A'; |
| |
| /** |
| * Unicode "Left-To-Right Mark" (LRM) character. |
| */ |
| public static final char LRM = '\u200E'; |
| |
| /** |
| * String representation of LRM. |
| */ |
| public static final String LRM_STRING = Character.toString(LRM); |
| |
| /** |
| * Unicode "Pop Directional Formatting" (PDF) character. |
| */ |
| public static final char PDF = '\u202C'; |
| |
| /** |
| * "right" string constant. |
| */ |
| public static final String RIGHT = "right"; |
| |
| /** |
| * Unicode "Right-To-Left Embedding" (RLE) character. |
| */ |
| public static final char RLE = '\u202B'; |
| |
| /** |
| * Unicode "Right-To-Left Mark" (RLM) character. |
| */ |
| public static final char RLM = '\u200F'; |
| |
| /** |
| * String representation of RLM. |
| */ |
| public static final String RLM_STRING = Character.toString(RLM); |
| |
| // Not instantiable. |
| private Format() { |
| } |
| } |
| |
| private boolean alwaysSpan; |
| private Direction contextDir; |
| |
| protected BidiFormatterBase(Direction contextDir, boolean alwaysSpan) { |
| this.contextDir = contextDir; |
| this.alwaysSpan = alwaysSpan; |
| } |
| |
| /** |
| * Like {@link #estimateDirection(String, boolean)}, but assumes {@code |
| * isHtml} is false. |
| * |
| * @param str String whose direction is to be estimated |
| * @return {@code str}'s estimated overall direction |
| */ |
| public Direction estimateDirection(String str) { |
| return BidiUtils.get().estimateDirection(str); |
| } |
| |
| /** |
| * Estimates the direction of a string using the best known general-purpose |
| * method, i.e. using relative word counts. Direction.DEFAULT return value |
| * indicates completely neutral input. |
| * |
| * @param str String whose direction is to be estimated |
| * @param isHtml Whether {@code str} is HTML / HTML-escaped |
| * @return {@code str}'s estimated overall direction |
| */ |
| public Direction estimateDirection(String str, boolean isHtml) { |
| return BidiUtils.get().estimateDirection(str, isHtml); |
| } |
| |
| /** |
| * Returns whether the span structure added by the formatter should be stable, |
| * i.e., spans added even when the direction does not need to be declared. |
| */ |
| public boolean getAlwaysSpan() { |
| return alwaysSpan; |
| } |
| |
| /** |
| * Returns the context direction. |
| */ |
| public Direction getContextDir() { |
| return contextDir; |
| } |
| |
| /** |
| * Returns whether the context direction is RTL. |
| */ |
| public boolean isRtlContext() { |
| return contextDir == Direction.RTL; |
| } |
| |
| /** |
| * @see BidiFormatter#dirAttr(String, boolean) |
| * |
| * @param str String whose direction is to be estimated |
| * @param isHtml Whether {@code str} is HTML / HTML-escaped |
| * @return "dir=rtl" for RTL text in non-RTL context; "dir=ltr" for LTR text |
| * in non-LTR context; else, the empty string. |
| */ |
| protected String dirAttrBase(String str, boolean isHtml) { |
| return knownDirAttrBase(BidiUtils.get().estimateDirection(str, isHtml)); |
| } |
| |
| /** |
| * @see BidiFormatter#endEdge |
| */ |
| protected String endEdgeBase() { |
| return contextDir == Direction.RTL ? Format.LEFT : Format.RIGHT; |
| } |
| |
| /** |
| * @see BidiFormatter#knownDirAttr(HasDirection.Direction) |
| * |
| * @param dir Given direction |
| * @return "dir=rtl" for RTL text in non-RTL context; "dir=ltr" for LTR text |
| * in non-LTR context; else, the empty string. |
| */ |
| protected String knownDirAttrBase(Direction dir) { |
| if (dir != contextDir) { |
| return dir == Direction.LTR ? "dir=ltr" : dir == Direction.RTL |
| ? "dir=rtl" : ""; |
| } |
| return ""; |
| } |
| |
| /** |
| * @see BidiFormatter#markAfter(String, boolean) |
| * |
| * @param str String after which the mark may need to appear |
| * @param isHtml Whether {@code str} is HTML / HTML-escaped |
| * @return LRM for RTL text in LTR context; RLM for LTR text in RTL context; |
| * else, the empty string. |
| */ |
| protected String markAfterBase(String str, boolean isHtml) { |
| str = BidiUtils.get().stripHtmlIfNeeded(str, isHtml); |
| return dirResetIfNeeded(str, BidiUtils.get().estimateDirection(str), false, |
| true); |
| } |
| |
| /** |
| * @see BidiFormatter#mark() |
| */ |
| protected String markBase() { |
| return contextDir == Direction.LTR ? Format.LRM_STRING |
| : contextDir == Direction.RTL ? Format.RLM_STRING : ""; |
| } |
| |
| /** |
| * @see BidiFormatter#spanWrap(String, boolean, boolean) |
| * |
| * @param str The input string |
| * @param isHtml Whether {@code str} is HTML / HTML-escaped |
| * @param dirReset Whether to append a trailing unicode bidi mark matching the |
| * context direction, when needed, to prevent the possible garbling |
| * of whatever may follow {@code str} |
| * @return Input string after applying the above processing. |
| */ |
| @IsSafeHtml |
| protected String spanWrapBase(@IsSafeHtml String str, boolean isHtml, boolean dirReset) { |
| Direction dir = BidiUtils.get().estimateDirection(str, isHtml); |
| return spanWrapWithKnownDirBase(dir, str, isHtml, dirReset); |
| } |
| |
| /** |
| * @see BidiFormatter#spanWrapWithKnownDir(HasDirection.Direction, String, boolean, boolean) |
| * |
| * @param dir {@code str}'s direction |
| * @param str The input string |
| * @param isHtml Whether {@code str} is HTML / HTML-escaped |
| * @param dirReset Whether to append a trailing unicode bidi mark matching the |
| * context direction, when needed, to prevent the possible garbling |
| * of whatever may follow {@code str} |
| * @return Input string after applying the above processing. |
| */ |
| @SuppressIsSafeHtmlCastCheck |
| @IsSafeHtml |
| protected String spanWrapWithKnownDirBase( |
| Direction dir, @IsSafeHtml String str, boolean isHtml, boolean dirReset) { |
| boolean dirCondition = dir != Direction.DEFAULT && dir != contextDir; |
| String origStr = str; |
| if (!isHtml) { |
| str = SafeHtmlUtils.htmlEscape(str); |
| } |
| |
| StringBuilder result = new StringBuilder(); |
| if (alwaysSpan || dirCondition) { |
| result.append("<span"); |
| if (dirCondition) { |
| result.append(" "); |
| result.append(dir == Direction.RTL ? "dir=rtl" : "dir=ltr"); |
| } |
| result.append(">" + str + "</span>"); |
| } else { |
| result.append(str); |
| } |
| // origStr is passed (more efficient when isHtml is false). |
| result.append(dirResetIfNeeded(origStr, dir, isHtml, dirReset)); |
| return result.toString(); |
| } |
| |
| /** |
| * @see BidiFormatter#startEdge |
| */ |
| protected String startEdgeBase() { |
| return contextDir == Direction.RTL ? Format.RIGHT : Format.LEFT; |
| } |
| |
| /** |
| * @see BidiFormatter#unicodeWrap(String, boolean, boolean) |
| * |
| * @param str The input string |
| * @param isHtml Whether {@code str} is HTML / HTML-escaped |
| * @param dirReset Whether to append a trailing unicode bidi mark matching the |
| * context direction, when needed, to prevent the possible garbling |
| * of whatever may follow {@code str} |
| * @return Input string after applying the above processing. |
| */ |
| protected String unicodeWrapBase(String str, boolean isHtml, |
| boolean dirReset) { |
| Direction dir = BidiUtils.get().estimateDirection(str, isHtml); |
| return unicodeWrapWithKnownDirBase(dir, str, isHtml, dirReset); |
| } |
| |
| /** |
| * @see BidiFormatter#unicodeWrapWithKnownDir(HasDirection.Direction, String, boolean, boolean) |
| * |
| * @param dir {@code str}'s direction |
| * @param str The input string |
| * @param isHtml Whether {@code str} is HTML / HTML-escaped |
| * @param dirReset Whether to append a trailing unicode bidi mark matching the |
| * context direction, when needed, to prevent the possible garbling |
| * of whatever may follow {@code str} |
| * @return Input string after applying the above processing. |
| */ |
| protected String unicodeWrapWithKnownDirBase(Direction dir, String str, |
| boolean isHtml, boolean dirReset) { |
| StringBuilder result = new StringBuilder(); |
| if (dir != Direction.DEFAULT && dir != contextDir) { |
| result.append(dir == Direction.RTL ? Format.RLE : Format.LRE); |
| result.append(str); |
| result.append(Format.PDF); |
| } else { |
| result.append(str); |
| } |
| |
| result.append(dirResetIfNeeded(str, dir, isHtml, dirReset)); |
| return result.toString(); |
| } |
| |
| /** |
| * Returns a unicode BiDi mark matching the context direction (LRM or RLM) if |
| * {@code dirReset}, and if the overall direction or the exit direction of |
| * {@code str} are opposite to the context direction. Otherwise returns the |
| * empty string. |
| * |
| * @param str The input string |
| * @param dir {@code str}'s overall direction |
| * @param isHtml Whether {@code str} is HTML / HTML-escaped |
| * @param dirReset Whether to perform the reset |
| * @return A unicode BiDi mark or the empty string. |
| */ |
| private String dirResetIfNeeded(String str, Direction dir, boolean isHtml, |
| boolean dirReset) { |
| // endsWithRtl and endsWithLtr are called only if needed (short-circuit). |
| if (dirReset |
| && ((contextDir == Direction.LTR && |
| (dir == Direction.RTL || |
| BidiUtils.get().endsWithRtl(str, isHtml))) || |
| (contextDir == Direction.RTL && |
| (dir == Direction.LTR || |
| BidiUtils.get().endsWithLtr(str, isHtml))))) { |
| return contextDir == Direction.LTR ? Format.LRM_STRING |
| : Format.RLM_STRING; |
| } else { |
| return ""; |
| } |
| } |
| } |