<commit_before>/**
 * Copyright (c) 2015-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */

package com.facebook.react.views.text;

import javax.annotation.Nullable;

import java.util.ArrayList;
import java.util.List;

import android.graphics.Typeface;
import android.text.BoringLayout;
import android.text.Layout;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.text.style.AbsoluteSizeSpan;
import android.text.style.BackgroundColorSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.StrikethroughSpan;
import android.text.style.UnderlineSpan;
import android.view.Gravity;
import android.widget.TextView;

import com.facebook.csslayout.CSSDirection;
import com.facebook.csslayout.CSSConstants;
import com.facebook.csslayout.CSSMeasureMode;
import com.facebook.csslayout.CSSNode;
import com.facebook.csslayout.MeasureOutput;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.common.annotations.VisibleForTesting;
import com.facebook.react.uimanager.IllegalViewOperationException;
import com.facebook.react.uimanager.LayoutShadowNode;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.ReactShadowNode;
import com.facebook.react.uimanager.UIViewOperationQueue;
import com.facebook.react.uimanager.ViewDefaults;
import com.facebook.react.uimanager.ViewProps;
import com.facebook.react.uimanager.annotations.ReactProp;

/**
 * {@link ReactShadowNode} class for spannable text view.
 * <p/>
 * This node calculates {@link Spannable} based on subnodes of the same type and passes the
 * resulting object down to textview's shadowview and actual native {@link TextView} instance. It is
 * important to keep in mind that {@link Spannable} is calculated only on layout step, so if there
 * are any text properties that may/should affect the result of {@link Spannable} they should be set
 * in a corresponding {@link ReactTextShadowNode}. Resulting {@link Spannable} object is then then
 * passed as "computedDataFromMeasure" down to shadow and native view.
 * <p/>
 * TODO(7255858): Rename *CSSNode to *ShadowView (or sth similar) as it's no longer is used solely
 * for layouting
 */
public class ReactTextShadowNode extends LayoutShadowNode {

  private static final String INLINE_IMAGE_PLACEHOLDER = "I";
  public static final int UNSET = -1;

  @VisibleForTesting
  public static final String PROP_TEXT = "text";

  public static final String PROP_SHADOW_OFFSET = "textShadowOffset";
  public static final String PROP_SHADOW_OFFSET_WIDTH = "width";
  public static final String PROP_SHADOW_OFFSET_HEIGHT = "height";
  public static final String PROP_SHADOW_RADIUS = "textShadowRadius";
  public static final String PROP_SHADOW_COLOR = "textShadowColor";

  public static final int DEFAULT_TEXT_SHADOW_COLOR = 0x55000000;

  private static final TextPaint sTextPaintInstance = new TextPaint();

  static {
    sTextPaintInstance.setFlags(TextPaint.ANTI_ALIAS_FLAG);
  }

  private static class SetSpanOperation {
    protected int start, end;
    protected Object what;
    SetSpanOperation(int start, int end, Object what) {
      this.start = start;
      this.end = end;
      this.what = what;
    }
    public void execute(SpannableStringBuilder sb) {
      // All spans will automatically extend to the right of the text, but not the left - except
      // for spans that start at the beginning of the text.
      int spanFlags = Spannable.SPAN_EXCLUSIVE_INCLUSIVE;
      if (start == 0) {
        spanFlags = Spannable.SPAN_INCLUSIVE_INCLUSIVE;
      }
      sb.setSpan(what, start, end, spanFlags);
    }
  }

  private static void buildSpannedFromTextCSSNode(
      ReactTextShadowNode textCSSNode,
      SpannableStringBuilder sb,
      List<SetSpanOperation> ops) {
    int start = sb.length();
    if (textCSSNode.mText != null) {
      sb.append(textCSSNode.mText);
    }
    for (int i = 0, length = textCSSNode.getChildCount(); i < length; i++) {
      CSSNode child = textCSSNode.getChildAt(i);
      if (child instanceof ReactTextShadowNode) {
        buildSpannedFromTextCSSNode((ReactTextShadowNode) child, sb, ops);
      } else if (child instanceof ReactTextInlineImageShadowNode) {
        // We make the image take up 1 character in the span and put a corresponding character into
        // the text so that the image doesn't run over any following text.
        sb.append(INLINE_IMAGE_PLACEHOLDER);
        ops.add(
          new SetSpanOperation(
            sb.length() - INLINE_IMAGE_PLACEHOLDER.length(),
            sb.length(),
            ((ReactTextInlineImageShadowNode) child).buildInlineImageSpan()));
      } else {
        throw new IllegalViewOperationException("Unexpected view type nested under text node: "
                + child.getClass());
      }
      ((ReactShadowNode) child).markUpdateSeen();
    }
    int end = sb.length();
    if (end >= start) {
      if (textCSSNode.mIsColorSet) {
        ops.add(new SetSpanOperation(start, end, new ForegroundColorSpan(textCSSNode.mColor)));
      }
      if (textCSSNode.mIsBackgroundColorSet) {
        ops.add(new SetSpanOperation(
                start,
                end,
                new BackgroundColorSpan(textCSSNode.mBackgroundColor)));
      }
      if (textCSSNode.mFontSize != UNSET) {
        ops.add(new SetSpanOperation(start, end, new AbsoluteSizeSpan(textCSSNode.mFontSize)));
      }
      if (textCSSNode.mFontStyle != UNSET ||
          textCSSNode.mFontWeight != UNSET ||
          textCSSNode.mFontFamily != null) {
        ops.add(new SetSpanOperation(
                start,
                end,
                new CustomStyleSpan(
                    textCSSNode.mFontStyle,
                    textCSSNode.mFontWeight,
                    textCSSNode.mFontFamily,
                    textCSSNode.getThemedContext().getAssets())));
      }
      if (textCSSNode.mIsUnderlineTextDecorationSet) {
        ops.add(new SetSpanOperation(start, end, new UnderlineSpan()));
      }
      if (textCSSNode.mIsLineThroughTextDecorationSet) {
        ops.add(new SetSpanOperation(start, end, new StrikethroughSpan()));
      }
      if (textCSSNode.mTextShadowOffsetDx != 0 || textCSSNode.mTextShadowOffsetDy != 0) {
        ops.add(new SetSpanOperation(
                start,
                end,
                new ShadowStyleSpan(
                    textCSSNode.mTextShadowOffsetDx,
                    textCSSNode.mTextShadowOffsetDy,
                    textCSSNode.mTextShadowRadius,
                    textCSSNode.mTextShadowColor)));
      }
      ops.add(new SetSpanOperation(start, end, new ReactTagSpan(textCSSNode.getReactTag())));
    }
  }

  protected static Spannable fromTextCSSNode(ReactTextShadowNode textCSSNode) {
    SpannableStringBuilder sb = new SpannableStringBuilder();
    // TODO(5837930): Investigate whether it's worth optimizing this part and do it if so

    // The {@link SpannableStringBuilder} implementation require setSpan operation to be called
    // up-to-bottom, otherwise all the spannables that are withing the region for which one may set
    // a new spannable will be wiped out
    List<SetSpanOperation> ops = new ArrayList<>();
    buildSpannedFromTextCSSNode(textCSSNode, sb, ops);
    if (textCSSNode.mFontSize == UNSET) {
      sb.setSpan(
          new AbsoluteSizeSpan((int) Math.ceil(PixelUtil.toPixelFromSP(ViewDefaults.FONT_SIZE_SP))),
          0,
          sb.length(),
          Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
    }

    textCSSNode.mContainsImages = false;
    textCSSNode.mHeightOfTallestInlineImage = Float.NaN;

    // While setting the Spans on the final text, we also check whether any of them are images
    for (int i = ops.size() - 1; i >= 0; i--) {
      SetSpanOperation op = ops.get(i);
      if (op.what instanceof TextInlineImageSpan) {
        int height = ((TextInlineImageSpan)op.what).getHeight();
        textCSSNode.mContainsImages = true;
        if (Float.isNaN(textCSSNode.mHeightOfTallestInlineImage) || height > textCSSNode.mHeightOfTallestInlineImage) {
          textCSSNode.mHeightOfTallestInlineImage = height;
        }
      }
      op.execute(sb);
    }
    return sb;
  }

  private static final CSSNode.MeasureFunction TEXT_MEASURE_FUNCTION =
      new CSSNode.MeasureFunction() {
        @Override
        public void measure(
            CSSNode node,
            float width,
            CSSMeasureMode widthMode,
            float height,
            CSSMeasureMode heightMode,
            MeasureOutput measureOutput) {
          // TODO(5578671): Handle text direction (see View#getTextDirectionHeuristic)
          ReactTextShadowNode reactCSSNode = (ReactTextShadowNode) node;
          TextPaint textPaint = sTextPaintInstance;
          Layout layout;
          Spanned text = Assertions.assertNotNull(
              reactCSSNode.mPreparedSpannableText,
              "Spannable element has not been prepared in onBeforeLayout");
          BoringLayout.Metrics boring = BoringLayout.isBoring(text, textPaint);
          float desiredWidth = boring == null ?
              Layout.getDesiredWidth(text, textPaint) : Float.NaN;

          // technically, width should never be negative, but there is currently a bug in
          boolean unconstrainedWidth = widthMode == CSSMeasureMode.UNDEFINED || width < 0;

          float effectiveLineHeight = reactCSSNode.getEffectiveLineHeight();
          float lineSpacingExtra = 0;
          float lineSpacingMultiplier = 1;
          if (!Float.isNaN(effectiveLineHeight)) {
            lineSpacingExtra = effectiveLineHeight;
            lineSpacingMultiplier = 0;
          }

          if (boring == null &&
              (unconstrainedWidth ||
                  (!CSSConstants.isUndefined(desiredWidth) && desiredWidth <= width))) {
            // Is used when the width is not known and the text is not boring, ie. if it contains
            // unicode characters.
            layout = new StaticLayout(
                text,
                textPaint,
                (int) Math.ceil(desiredWidth),
                Layout.Alignment.ALIGN_NORMAL,
                lineSpacingMultiplier,
                lineSpacingExtra,
                true);
          } else if (boring != null && (unconstrainedWidth || boring.width <= width)) {
            // Is used for single-line, boring text when the width is either unknown or bigger
            // than the width of the text.
            layout = BoringLayout.make(
                text,
                textPaint,
                boring.width,
                Layout.Alignment.ALIGN_NORMAL,
                lineSpacingMultiplier,
                lineSpacingExtra,
                boring,
                true);
          } else {
            // Is used for multiline, boring text and the width is known.
            layout = new StaticLayout(
                text,
                textPaint,
                (int) width,
                Layout.Alignment.ALIGN_NORMAL,
                lineSpacingMultiplier,
                lineSpacingExtra,
                true);
          }

          measureOutput.height = layout.getHeight();
          measureOutput.width = layout.getWidth();
          if (reactCSSNode.mNumberOfLines != UNSET &&
              reactCSSNode.mNumberOfLines < layout.getLineCount()) {
            measureOutput.height = layout.getLineBottom(reactCSSNode.mNumberOfLines - 1);
          }
        }
      };

  /**
   * Return -1 if the input string is not a valid numeric fontWeight (100, 200, ..., 900), otherwise
   * return the weight.
   *
   * This code is duplicated in ReactTextInputManager
   * TODO: Factor into a common place they can both use
   */
  private static int parseNumericFontWeight(String fontWeightString) {
    // This should be much faster than using regex to verify input and Integer.parseInt
    return fontWeightString.length() == 3 && fontWeightString.endsWith("00")
        && fontWeightString.charAt(0) <= '9' && fontWeightString.charAt(0) >= '1' ?
        100 * (fontWeightString.charAt(0) - '0') : -1;
  }

  private float mLineHeight = Float.NaN;
  private boolean mIsColorSet = false;
  private int mColor;
  private boolean mIsBackgroundColorSet = false;
  private int mBackgroundColor;

  protected int mNumberOfLines = UNSET;
  protected int mFontSize = UNSET;
  protected int mTextAlign = UNSET;

  private float mTextShadowOffsetDx = 0;
  private float mTextShadowOffsetDy = 0;
  private float mTextShadowRadius = 1;
  private int mTextShadowColor = DEFAULT_TEXT_SHADOW_COLOR;

  private boolean mIsUnderlineTextDecorationSet = false;
  private boolean mIsLineThroughTextDecorationSet = false;

  /**
   * mFontStyle can be {@link Typeface#NORMAL} or {@link Typeface#ITALIC}.
   * mFontWeight can be {@link Typeface#NORMAL} or {@link Typeface#BOLD}.
   */
  private int mFontStyle = UNSET;
  private int mFontWeight = UNSET;
  /**
   * NB: If a font family is used that does not have a style in a certain Android version (ie.
   * monospace bold pre Android 5.0), that style (ie. bold) will not be inherited by nested Text
   * nodes. To retain that style, you have to add it to those nodes explicitly.
   * Example, Android 4.4:
   * <Text style={{fontFamily="serif" fontWeight="bold"}}>Bold Text</Text>
   *   <Text style={{fontFamily="sans-serif"}}>Bold Text</Text>
   *     <Text style={{fontFamily="serif}}>Bold Text</Text>
   *
   * <Text style={{fontFamily="monospace" fontWeight="bold"}}>Not Bold Text</Text>
   *   <Text style={{fontFamily="sans-serif"}}>Not Bold Text</Text>
   *     <Text style={{fontFamily="serif}}>Not Bold Text</Text>
   *<commit_msg>Fix UNSET problem for text alignment change<commit_after>/**
 * Copyright (c) 2015-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */

package com.facebook.react.views.text;

import javax.annotation.Nullable;

import java.util.ArrayList;
import java.util.List;

import android.graphics.Typeface;
import android.text.BoringLayout;
import android.text.Layout;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.text.style.AbsoluteSizeSpan;
import android.text.style.BackgroundColorSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.StrikethroughSpan;
import android.text.style.UnderlineSpan;
import android.view.Gravity;
import android.widget.TextView;

import com.facebook.csslayout.CSSDirection;
import com.facebook.csslayout.CSSConstants;
import com.facebook.csslayout.CSSMeasureMode;
import com.facebook.csslayout.CSSNode;
import com.facebook.csslayout.MeasureOutput;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.common.annotations.VisibleForTesting;
import com.facebook.react.uimanager.IllegalViewOperationException;
import com.facebook.react.uimanager.LayoutShadowNode;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.ReactShadowNode;
import com.facebook.react.uimanager.UIViewOperationQueue;
import com.facebook.react.uimanager.ViewDefaults;
import com.facebook.react.uimanager.ViewProps;
import com.facebook.react.uimanager.annotations.ReactProp;

/**
 * {@link ReactShadowNode} class for spannable text view.
 * <p/>
 * This node calculates {@link Spannable} based on subnodes of the same type and passes the
 * resulting object down to textview's shadowview and actual native {@link TextView} instance. It is
 * important to keep in mind that {@link Spannable} is calculated only on layout step, so if there
 * are any text properties that may/should affect the result of {@link Spannable} they should be set
 * in a corresponding {@link ReactTextShadowNode}. Resulting {@link Spannable} object is then then
 * passed as "computedDataFromMeasure" down to shadow and native view.
 * <p/>
 * TODO(7255858): Rename *CSSNode to *ShadowView (or sth similar) as it's no longer is used solely
 * for layouting
 */
public class ReactTextShadowNode extends LayoutShadowNode {

  private static final String INLINE_IMAGE_PLACEHOLDER = "I";
  public static final int UNSET = -1;

  @VisibleForTesting
  public static final String PROP_TEXT = "text";

  public static final String PROP_SHADOW_OFFSET = "textShadowOffset";
  public static final String PROP_SHADOW_OFFSET_WIDTH = "width";
  public static final String PROP_SHADOW_OFFSET_HEIGHT = "height";
  public static final String PROP_SHADOW_RADIUS = "textShadowRadius";
  public static final String PROP_SHADOW_COLOR = "textShadowColor";

  public static final int DEFAULT_TEXT_SHADOW_COLOR = 0x55000000;

  private static final TextPaint sTextPaintInstance = new TextPaint();

  static {
    sTextPaintInstance.setFlags(TextPaint.ANTI_ALIAS_FLAG);
  }

  private static class SetSpanOperation {
    protected int start, end;
    protected Object what;
    SetSpanOperation(int start, int end, Object what) {
      this.start = start;
      this.end = end;
      this.what = what;
    }
    public void execute(SpannableStringBuilder sb) {
      // All spans will automatically extend to the right of the text, but not the left - except
      // for spans that start at the beginning of the text.
      int spanFlags = Spannable.SPAN_EXCLUSIVE_INCLUSIVE;
      if (start == 0) {
        spanFlags = Spannable.SPAN_INCLUSIVE_INCLUSIVE;
      }
      sb.setSpan(what, start, end, spanFlags);
    }
  }

  private static void buildSpannedFromTextCSSNode(
      ReactTextShadowNode textCSSNode,
      SpannableStringBuilder sb,
      List<SetSpanOperation> ops) {
    int start = sb.length();
    if (textCSSNode.mText != null) {
      sb.append(textCSSNode.mText);
    }
    for (int i = 0, length = textCSSNode.getChildCount(); i < length; i++) {
      CSSNode child = textCSSNode.getChildAt(i);
      if (child instanceof ReactTextShadowNode) {
        buildSpannedFromTextCSSNode((ReactTextShadowNode) child, sb, ops);
      } else if (child instanceof ReactTextInlineImageShadowNode) {
        // We make the image take up 1 character in the span and put a corresponding character into
        // the text so that the image doesn't run over any following text.
        sb.append(INLINE_IMAGE_PLACEHOLDER);
        ops.add(
          new SetSpanOperation(
            sb.length() - INLINE_IMAGE_PLACEHOLDER.length(),
            sb.length(),
            ((ReactTextInlineImageShadowNode) child).buildInlineImageSpan()));
      } else {
        throw new IllegalViewOperationException("Unexpected view type nested under text node: "
                + child.getClass());
      }
      ((ReactShadowNode) child).markUpdateSeen();
    }
    int end = sb.length();
    if (end >= start) {
      if (textCSSNode.mIsColorSet) {
        ops.add(new SetSpanOperation(start, end, new ForegroundColorSpan(textCSSNode.mColor)));
      }
      if (textCSSNode.mIsBackgroundColorSet) {
        ops.add(new SetSpanOperation(
                start,
                end,
                new BackgroundColorSpan(textCSSNode.mBackgroundColor)));
      }
      if (textCSSNode.mFontSize != UNSET) {
        ops.add(new SetSpanOperation(start, end, new AbsoluteSizeSpan(textCSSNode.mFontSize)));
      }
      if (textCSSNode.mFontStyle != UNSET ||
          textCSSNode.mFontWeight != UNSET ||
          textCSSNode.mFontFamily != null) {
        ops.add(new SetSpanOperation(
                start,
                end,
                new CustomStyleSpan(
                    textCSSNode.mFontStyle,
                    textCSSNode.mFontWeight,
                    textCSSNode.mFontFamily,
                    textCSSNode.getThemedContext().getAssets())));
      }
      if (textCSSNode.mIsUnderlineTextDecorationSet) {
        ops.add(new SetSpanOperation(start, end, new UnderlineSpan()));
      }
      if (textCSSNode.mIsLineThroughTextDecorationSet) {
        ops.add(new SetSpanOperation(start, end, new StrikethroughSpan()));
      }
      if (textCSSNode.mTextShadowOffsetDx != 0 || textCSSNode.mTextShadowOffsetDy != 0) {
        ops.add(new SetSpanOperation(
                start,
                end,
                new ShadowStyleSpan(
                    textCSSNode.mTextShadowOffsetDx,
                    textCSSNode.mTextShadowOffsetDy,
                    textCSSNode.mTextShadowRadius,
                    textCSSNode.mTextShadowColor)));
      }
      ops.add(new SetSpanOperation(start, end, new ReactTagSpan(textCSSNode.getReactTag())));
    }
  }

  protected static Spannable fromTextCSSNode(ReactTextShadowNode textCSSNode) {
    SpannableStringBuilder sb = new SpannableStringBuilder();
    // TODO(5837930): Investigate whether it's worth optimizing this part and do it if so

    // The {@link SpannableStringBuilder} implementation require setSpan operation to be called
    // up-to-bottom, otherwise all the spannables that are withing the region for which one may set
    // a new spannable will be wiped out
    List<SetSpanOperation> ops = new ArrayList<>();
    buildSpannedFromTextCSSNode(textCSSNode, sb, ops);
    if (textCSSNode.mFontSize == UNSET) {
      sb.setSpan(
          new AbsoluteSizeSpan((int) Math.ceil(PixelUtil.toPixelFromSP(ViewDefaults.FONT_SIZE_SP))),
          0,
          sb.length(),
          Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
    }

    textCSSNode.mContainsImages = false;
    textCSSNode.mHeightOfTallestInlineImage = Float.NaN;

    // While setting the Spans on the final text, we also check whether any of them are images
    for (int i = ops.size() - 1; i >= 0; i--) {
      SetSpanOperation op = ops.get(i);
      if (op.what instanceof TextInlineImageSpan) {
        int height = ((TextInlineImageSpan)op.what).getHeight();
        textCSSNode.mContainsImages = true;
        if (Float.isNaN(textCSSNode.mHeightOfTallestInlineImage) || height > textCSSNode.mHeightOfTallestInlineImage) {
          textCSSNode.mHeightOfTallestInlineImage = height;
        }
      }
      op.execute(sb);
    }
    return sb;
  }

  private static final CSSNode.MeasureFunction TEXT_MEASURE_FUNCTION =
      new CSSNode.MeasureFunction() {
        @Override
        public void measure(
            CSSNode node,
            float width,
            CSSMeasureMode widthMode,
            float height,
            CSSMeasureMode heightMode,
            MeasureOutput measureOutput) {
          // TODO(5578671): Handle text direction (see View#getTextDirectionHeuristic)
          ReactTextShadowNode reactCSSNode = (ReactTextShadowNode) node;
          TextPaint textPaint = sTextPaintInstance;
          Layout layout;
          Spanned text = Assertions.assertNotNull(
              reactCSSNode.mPreparedSpannableText,
              "Spannable element has not been prepared in onBeforeLayout");
          BoringLayout.Metrics boring = BoringLayout.isBoring(text, textPaint);
          float desiredWidth = boring == null ?
              Layout.getDesiredWidth(text, textPaint) : Float.NaN;

          // technically, width should never be negative, but there is currently a bug in
          boolean unconstrainedWidth = widthMode == CSSMeasureMode.UNDEFINED || width < 0;

          float effectiveLineHeight = reactCSSNode.getEffectiveLineHeight();
          float lineSpacingExtra = 0;
          float lineSpacingMultiplier = 1;
          if (!Float.isNaN(effectiveLineHeight)) {
            lineSpacingExtra = effectiveLineHeight;
            lineSpacingMultiplier = 0;
          }

          if (boring == null &&
              (unconstrainedWidth ||
                  (!CSSConstants.isUndefined(desiredWidth) && desiredWidth <= width))) {
            // Is used when the width is not known and the text is not boring, ie. if it contains
            // unicode characters.
            layout = new StaticLayout(
                text,
                textPaint,
                (int) Math.ceil(desiredWidth),
                Layout.Alignment.ALIGN_NORMAL,
                lineSpacingMultiplier,
                lineSpacingExtra,
                true);
          } else if (boring != null && (unconstrainedWidth || boring.width <= width)) {
            // Is used for single-line, boring text when the width is either unknown or bigger
            // than the width of the text.
            layout = BoringLayout.make(
                text,
                textPaint,
                boring.width,
                Layout.Alignment.ALIGN_NORMAL,
                lineSpacingMultiplier,
                lineSpacingExtra,
                boring,
                true);
          } else {
            // Is used for multiline, boring text and the width is known.
            layout = new StaticLayout(
                text,
                textPaint,
                (int) width,
                Layout.Alignment.ALIGN_NORMAL,
                lineSpacingMultiplier,
                lineSpacingExtra,
                true);
          }

          measureOutput.height = layout.getHeight();
          measureOutput.width = layout.getWidth();
          if (reactCSSNode.mNumberOfLines != UNSET &&
              reactCSSNode.mNumberOfLines < layout.getLineCount()) {
            measureOutput.height = layout.getLineBottom(reactCSSNode.mNumberOfLines - 1);
          }
        }
      };

  /**
   * Return -1 if the input string is not a valid numeric fontWeight (100, 200, ..., 900), otherwise
   * return the weight.
   *
   * This code is duplicated in ReactTextInputManager
   * TODO: Factor into a common place they can both use
   */
  private static int parseNumericFontWeight(String fontWeightString) {
    // This should be much faster than using regex to verify input and Integer.parseInt
    return fontWeightString.length() == 3 && fontWeightString.endsWith("00")
        && fontWeightString.charAt(0) <= '9' && fontWeightString.charAt(0) >= '1' ?
        100 * (fontWeightString.charAt(0) - '0') : -1;
  }

  private float mLineHeight = Float.NaN;
  private boolean mIsColorSet = false;
  private int mColor;
  private boolean mIsBackgroundColorSet = false;
  private int mBackgroundColor;

  protected int mNumberOfLines = UNSET;
  protected int mFontSize = UNSET;
  protected int mTextAlign = Gravity.NO_GRAVITY;

  private float mTextShadowOffsetDx = 0;
  private float mTextShadowOffsetDy = 0;
  private float mTextShadowRadius = 1;
  private int mTextShadowColor = DEFAULT_TEXT_SHADOW_COLOR;

  private boolean mIsUnderlineTextDecorationSet = false;
  private boolean mIsLineThroughTextDecorationSet = false;

  /**
   * mFontStyle can be {@link Typeface#NORMAL} or {@link Typeface#ITALIC}.
   * mFontWeight can be {@link Typeface#NORMAL} or {@link Typeface#BOLD}.
   */
  private int mFontStyle = UNSET;
  private int mFontWeight = UNSET;
  /**
   * NB: If a font family is used that does not have a style in a certain Android version (ie.
   * monospace bold pre Android 5.0), that style (ie. bold) will not be inherited by nested Text
   * nodes. To retain that style, you have to add it to those nodes explicitly.
   * Example, Android 4.4:
   * <Text style={{fontFamily="serif" fontWeight="bold"}}>Bold Text</Text>
   *   <Text style={{fontFamily="sans-serif"}}>Bold Text</Text>
   *     <Text style={{fontFamily="serif}}>Bold Text</Text>
   *
   * <Text style={{fontFamily="monospace" fontWeight="bold"}}>Not Bold Text</Text>
   *   <Text style={{fontFamily="sans-serif"}}>Not Bold Text</Text>
   *     <Text style={{fontFamily="serif}}>Not Bold Text</Text>
   *
{'fraction_non_alphanumeric': 0.08645033440673767, 'fraction_numerical': 0.005520365193389717, 'mean_word_length': 2.814794816414687, 'pattern_counts': {'":': 0, '<': 49, '<?xml version=': 0, '>': 45, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2773161', 'n_tokens_mistral': 8051, 'n_tokens_neox': 7788, 'n_words': 2166}