part of dart._engine;
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.




class EngineLineMetrics implements ui.LineMetrics {
  const EngineLineMetrics({
    required this.hardBreak,
    required this.ascent,
    required this.descent,
    required this.unscaledAscent,
    required this.height,
    required this.width,
    required this.left,
    required this.baseline,
    required this.lineNumber,
  });

  @override
  final bool hardBreak;

  @override
  final double ascent;

  @override
  final double descent;

  @override
  final double unscaledAscent;

  @override
  final double height;

  @override
  final double width;

  @override
  final double left;

  @override
  final double baseline;

  @override
  final int lineNumber;

  @override
  int get hashCode => Object.hash(
        hardBreak,
        ascent,
        descent,
        unscaledAscent,
        height,
        width,
        left,
        baseline,
        lineNumber,
      );

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) {
      return true;
    }
    if (other.runtimeType != runtimeType) {
      return false;
    }
    return other is EngineLineMetrics &&
        other.hardBreak == hardBreak &&
        other.ascent == ascent &&
        other.descent == descent &&
        other.unscaledAscent == unscaledAscent &&
        other.height == height &&
        other.width == width &&
        other.left == left &&
        other.baseline == baseline &&
        other.lineNumber == lineNumber;
  }

  @override
  String toString() {
    if (assertionsEnabled) {
      return 'LineMetrics(hardBreak: $hardBreak, '
          'ascent: $ascent, '
          'descent: $descent, '
          'unscaledAscent: $unscaledAscent, '
          'height: $height, '
          'width: $width, '
          'left: $left, '
          'baseline: $baseline, '
          'lineNumber: $lineNumber)';
    } else {
      return super.toString();
    }
  }
}

class ParagraphLine {
  ParagraphLine({
    required bool hardBreak,
    required double ascent,
    required double descent,
    required double height,
    required double width,
    required double left,
    required double baseline,
    required int lineNumber,
    required this.ellipsis,
    required this.startIndex,
    required this.endIndex,
    required this.endIndexWithoutNewlines,
    required this.widthWithTrailingSpaces,
    required this.boxes,
    required this.spaceBoxCount,
    required this.trailingSpaceBoxCount,
    this.displayText,
  }) : lineMetrics = EngineLineMetrics(
          hardBreak: hardBreak,
          ascent: ascent,
          descent: descent,
          unscaledAscent: ascent,
          height: height,
          width: width,
          left: left,
          baseline: baseline,
          lineNumber: lineNumber,
        );

  /// Metrics for this line of the paragraph.
  final EngineLineMetrics lineMetrics;

  /// The string to be displayed as an overflow indicator.
  ///
  /// When the value is non-null, it means this line is overflowing and the
  /// [ellipsis] needs to be displayed at the end of it.
  final String? ellipsis;

  /// The index (inclusive) in the text where this line begins.
  final int startIndex;

  /// The index (exclusive) in the text where this line ends.
  ///
  /// When the line contains an overflow, then [endIndex] goes until the end of
  /// the text and doesn't stop at the overflow cutoff.
  final int endIndex;

  /// The index (exclusive) in the text where this line ends, ignoring newline
  /// characters.
  final int endIndexWithoutNewlines;

  /// The full width of the line including all trailing space but not new lines.
  ///
  /// The difference between [width] and [widthWithTrailingSpaces] is that
  /// [widthWithTrailingSpaces] includes trailing spaces in the width
  /// calculation while [width] doesn't.
  ///
  /// For alignment purposes for example, the [width] property is the right one
  /// to use because trailing spaces shouldn't affect the centering of text.
  /// But for placing cursors in text fields, we do care about trailing
  /// spaces so [widthWithTrailingSpaces] is more suitable.
  final double widthWithTrailingSpaces;

  /// The list of boxes representing the entire line, possibly across multiple
  /// spans.
  final List<RangeBox> boxes;

  /// The number of boxes that are space-only.
  final int spaceBoxCount;

  /// The number of trailing boxes that are space-only.
  final int trailingSpaceBoxCount;

  /// The text to be rendered on the screen representing this line.
  final String? displayText;

  /// The number of space-only boxes excluding trailing spaces.
  int get nonTrailingSpaceBoxCount => spaceBoxCount - trailingSpaceBoxCount;

  // Convenient getters for line metrics properties.

  bool get hardBreak => lineMetrics.hardBreak;
  double get ascent => lineMetrics.ascent;
  double get descent => lineMetrics.descent;
  double get unscaledAscent => lineMetrics.unscaledAscent;
  double get height => lineMetrics.height;
  double get width => lineMetrics.width;
  double get left => lineMetrics.left;
  double get baseline => lineMetrics.baseline;
  int get lineNumber => lineMetrics.lineNumber;

  bool overlapsWith(int startIndex, int endIndex) {
    return startIndex < this.endIndex && this.startIndex < endIndex;
  }

  @override
  int get hashCode => Object.hash(
        lineMetrics,
        ellipsis,
        startIndex,
        endIndex,
        endIndexWithoutNewlines,
        widthWithTrailingSpaces,
        boxes,
        spaceBoxCount,
        trailingSpaceBoxCount,
        displayText,
      );

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) {
      return true;
    }
    if (other.runtimeType != runtimeType) {
      return false;
    }
    return other is ParagraphLine &&
        other.lineMetrics == lineMetrics &&
        other.ellipsis == ellipsis &&
        other.startIndex == startIndex &&
        other.endIndex == endIndex &&
        other.endIndexWithoutNewlines == endIndexWithoutNewlines &&
        other.widthWithTrailingSpaces == widthWithTrailingSpaces &&
        other.boxes == boxes &&
        other.spaceBoxCount == spaceBoxCount &&
        other.trailingSpaceBoxCount == trailingSpaceBoxCount &&
        other.displayText == displayText;
  }
}

/// The web implementation of [ui.ParagraphStyle].
class EngineParagraphStyle implements ui.ParagraphStyle {
  /// Creates a new instance of [EngineParagraphStyle].
  EngineParagraphStyle({
    this.textAlign,
    this.textDirection,
    this.maxLines,
    this.fontFamily,
    this.fontSize,
    this.height,
    ui.TextHeightBehavior? textHeightBehavior,
    this.fontWeight,
    this.fontStyle,
    ui.StrutStyle? strutStyle,
    this.ellipsis,
    this.locale,
  })  : _textHeightBehavior = textHeightBehavior,
        // TODO(mdebbar): add support for strut style., b/128317744
        _strutStyle = strutStyle as EngineStrutStyle?;

  final ui.TextAlign? textAlign;
  final ui.TextDirection? textDirection;
  final ui.FontWeight? fontWeight;
  final ui.FontStyle? fontStyle;
  final int? maxLines;
  final String? fontFamily;
  final double? fontSize;
  final double? height;
  final ui.TextHeightBehavior? _textHeightBehavior;
  final EngineStrutStyle? _strutStyle;
  final String? ellipsis;
  final ui.Locale? locale;

  // The effective style attributes should be consistent with paragraph_style.h.
  ui.TextAlign get effectiveTextAlign => textAlign ?? ui.TextAlign.start;
  ui.TextDirection get effectiveTextDirection =>
      textDirection ?? ui.TextDirection.ltr;

  double? get lineHeight {
    // TODO(mdebbar): Implement proper support for strut styles.
    // https://github.com/flutter/flutter/issues/32243
    final EngineStrutStyle? strutStyle = _strutStyle;
    final double? strutHeight = strutStyle?._height;
    if (strutStyle == null || strutHeight == null || strutHeight == 0) {
      // When there's no strut height, always use paragraph style height.
      return height;
    }
    if (strutStyle._forceStrutHeight == true) {
      // When strut height is forced, ignore paragraph style height.
      return strutHeight;
    }
    // In this case, strut height acts as a minimum height for all parts of the
    // paragraph. So we take the max of strut height and paragraph style height.
    return math.max(strutHeight, height ?? 0.0);
  }

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) {
      return true;
    }
    if (other.runtimeType != runtimeType) {
      return false;
    }
    return other is EngineParagraphStyle &&
        other.textAlign == textAlign &&
        other.textDirection == textDirection &&
        other.fontWeight == fontWeight &&
        other.fontStyle == fontStyle &&
        other.maxLines == maxLines &&
        other.fontFamily == fontFamily &&
        other.fontSize == fontSize &&
        other.height == height &&
        other._textHeightBehavior == _textHeightBehavior &&
        other.ellipsis == ellipsis &&
        other.locale == locale;
  }

  @override
  int get hashCode {
    return Object.hash(
        textAlign,
        textDirection,
        fontWeight,
        fontStyle,
        maxLines,
        fontFamily,
        fontSize,
        height,
        _textHeightBehavior,
        ellipsis,
        locale);
  }

  @override
  String toString() {
    if (assertionsEnabled) {
      final double? fontSize = this.fontSize;
      final double? height = this.height;
      return 'ParagraphStyle('
          'textAlign: ${textAlign ?? "unspecified"}, '
          'textDirection: ${textDirection ?? "unspecified"}, '
          'fontWeight: ${fontWeight ?? "unspecified"}, '
          'fontStyle: ${fontStyle ?? "unspecified"}, '
          'maxLines: ${maxLines ?? "unspecified"}, '
          'textHeightBehavior: ${_textHeightBehavior ?? "unspecified"}, '
          'fontFamily: ${fontFamily ?? "unspecified"}, '
          'fontSize: ${fontSize != null ? fontSize.toStringAsFixed(1) : "unspecified"}, '
          'height: ${height != null ? "${height.toStringAsFixed(1)}x" : "unspecified"}, '
          'ellipsis: ${ellipsis != null ? "\"$ellipsis\"" : "unspecified"}, '
          'locale: ${locale ?? "unspecified"}'
          ')';
    } else {
      return super.toString();
    }
  }
}

/// The web implementation of [ui.TextStyle].
class EngineTextStyle implements ui.TextStyle {
  /// Constructs an [EngineTextStyle] with all properties being required.
  ///
  /// This is good for call sites that need to be updated whenever a new
  /// property is added to [EngineTextStyle]. Non-updated call sites will fail
  /// the build otherwise.
  factory EngineTextStyle({
    required ui.Color? color,
    required ui.TextDecoration? decoration,
    required ui.Color? decorationColor,
    required ui.TextDecorationStyle? decorationStyle,
    required double? decorationThickness,
    required ui.FontWeight? fontWeight,
    required ui.FontStyle? fontStyle,
    required ui.TextBaseline? textBaseline,
    required String? fontFamily,
    required List<String>? fontFamilyFallback,
    required double? fontSize,
    required double? letterSpacing,
    required double? wordSpacing,
    required double? height,
    required ui.Locale? locale,
    required ui.Paint? background,
    required ui.Paint? foreground,
    required List<ui.Shadow>? shadows,
    required List<ui.FontFeature>? fontFeatures,
    required List<ui.FontVariation>? fontVariations,
  }) = EngineTextStyle.only;

  /// Constructs an [EngineTextStyle] with only the given properties.
  ///
  /// This constructor should be used sparingly in tests, for example. Or when
  /// we know for sure that not all properties are needed.
  EngineTextStyle.only({
    this.color,
    this.decoration,
    this.decorationColor,
    this.decorationStyle,
    this.decorationThickness,
    this.fontWeight,
    this.fontStyle,
    this.textBaseline,
    String? fontFamily,
    this.fontFamilyFallback,
    this.fontSize,
    this.letterSpacing,
    this.wordSpacing,
    this.height,
    this.locale,
    this.background,
    this.foreground,
    this.shadows,
    this.fontFeatures,
    this.fontVariations,
  })  : assert(
            color == null || foreground == null,
            'Cannot provide both a color and a foreground\n'
            'The color argument is just a shorthand for "foreground: Paint()..color = color".'),
        isFontFamilyProvided = fontFamily != null,
        fontFamily = fontFamily ?? '';

  /// Constructs an [EngineTextStyle] by reading properties from an
  /// [EngineParagraphStyle].
  factory EngineTextStyle.fromParagraphStyle(
    EngineParagraphStyle paragraphStyle,
  ) {
    return EngineTextStyle.only(
      fontWeight: paragraphStyle.fontWeight,
      fontStyle: paragraphStyle.fontStyle,
      fontFamily: paragraphStyle.fontFamily,
      fontSize: paragraphStyle.fontSize,
      height: paragraphStyle.height,
      locale: paragraphStyle.locale,
    );
  }

  final ui.Color? color;
  final ui.TextDecoration? decoration;
  final ui.Color? decorationColor;
  final ui.TextDecorationStyle? decorationStyle;
  final double? decorationThickness;
  final ui.FontWeight? fontWeight;
  final ui.FontStyle? fontStyle;
  final ui.TextBaseline? textBaseline;
  final bool isFontFamilyProvided;
  final String fontFamily;
  final List<String>? fontFamilyFallback;
  final List<ui.FontFeature>? fontFeatures;
  final List<ui.FontVariation>? fontVariations;
  final double? fontSize;
  final double? letterSpacing;
  final double? wordSpacing;
  final double? height;
  final ui.Locale? locale;
  final ui.Paint? background;
  final ui.Paint? foreground;
  final List<ui.Shadow>? shadows;

  String get effectiveFontFamily {
    if (assertionsEnabled) {
      // In the flutter tester environment, we use a predictable-size font
      // "Ahem". This makes widget tests predictable and less flaky.
      if (ui.debugEmulateFlutterTesterEnvironment) {
        return 'Ahem';
      }
    }
    if (fontFamily.isEmpty) {
      return FlutterViewEmbedder.defaultFontFamily;
    }
    return fontFamily;
  }

  String? _cssFontString;

  /// Font string to be used in CSS.
  ///
  /// See <https://developer.mozilla.org/en-US/docs/Web/CSS/font>.
  String get cssFontString {
    return _cssFontString ??= buildCssFontString(
      fontStyle: fontStyle,
      fontWeight: fontWeight,
      fontSize: fontSize,
      fontFamily: effectiveFontFamily,
    );
  }

  late final TextHeightStyle heightStyle = _createHeightStyle();

  TextHeightStyle _createHeightStyle() {
    return TextHeightStyle(
      fontFamily: effectiveFontFamily,
      fontSize: fontSize ?? FlutterViewEmbedder.defaultFontSize,
      height: height,
      // TODO(mdebbar): Pass the actual value when font features become supported
      //                https://github.com/flutter/flutter/issues/64595
      fontFeatures: null,
      fontVariations: null,
    );
  }

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) {
      return true;
    }
    if (other.runtimeType != runtimeType) {
      return false;
    }
    return other is EngineTextStyle &&
        other.color == color &&
        other.decoration == decoration &&
        other.decorationColor == decorationColor &&
        other.decorationStyle == decorationStyle &&
        other.fontWeight == fontWeight &&
        other.fontStyle == fontStyle &&
        other.textBaseline == textBaseline &&
        other.fontFamily == fontFamily &&
        other.fontSize == fontSize &&
        other.letterSpacing == letterSpacing &&
        other.wordSpacing == wordSpacing &&
        other.height == height &&
        other.locale == locale &&
        other.background == background &&
        other.foreground == foreground &&
        listEquals<ui.Shadow>(other.shadows, shadows) &&
        listEquals<String>(other.fontFamilyFallback, fontFamilyFallback);
  }

  @override
  int get hashCode => Object.hash(
        color,
        decoration,
        decorationColor,
        decorationStyle,
        decorationThickness,
        fontWeight,
        fontStyle,
        textBaseline,
        fontFamily,
        fontFamilyFallback,
        fontSize,
        letterSpacing,
        wordSpacing,
        height,
        locale,
        background,
        foreground,
        shadows,
      );

  @override
  String toString() {
    if (assertionsEnabled) {
      final List<String>? fontFamilyFallback = this.fontFamilyFallback;
      final double? fontSize = this.fontSize;
      final double? height = this.height;
      return 'TextStyle('
          'color: ${color ?? "unspecified"}, '
          'decoration: ${decoration ?? "unspecified"}, '
          'decorationColor: ${decorationColor ?? "unspecified"}, '
          'decorationStyle: ${decorationStyle ?? "unspecified"}, '
          'decorationThickness: ${decorationThickness ?? "unspecified"}, '
          'fontWeight: ${fontWeight ?? "unspecified"}, '
          'fontStyle: ${fontStyle ?? "unspecified"}, '
          'textBaseline: ${textBaseline ?? "unspecified"}, '
          'fontFamily: ${isFontFamilyProvided && fontFamily != '' ? fontFamily : "unspecified"}, '
          'fontFamilyFallback: ${isFontFamilyProvided && fontFamilyFallback != null && fontFamilyFallback.isNotEmpty ? fontFamilyFallback : "unspecified"}, '
          'fontSize: ${fontSize != null ? fontSize.toStringAsFixed(1) : "unspecified"}, '
          'letterSpacing: ${letterSpacing != null ? "${letterSpacing}x" : "unspecified"}, '
          'wordSpacing: ${wordSpacing != null ? "${wordSpacing}x" : "unspecified"}, '
          'height: ${height != null ? "${height.toStringAsFixed(1)}x" : "unspecified"}, '
          'locale: ${locale ?? "unspecified"}, '
          'background: ${background ?? "unspecified"}, '
          'foreground: ${foreground ?? "unspecified"}, '
          'shadows: ${shadows ?? "unspecified"}, '
          'fontFeatures: ${fontFeatures ?? "unspecified"}, '
          'fontVariations: ${fontVariations ?? "unspecified"}'
          ')';
    } else {
      return super.toString();
    }
  }
}

/// The web implementation of [ui.StrutStyle].
class EngineStrutStyle implements ui.StrutStyle {
  EngineStrutStyle({
    String? fontFamily,
    List<String>? fontFamilyFallback,
    double? fontSize,
    double? height,
    ui.TextLeadingDistribution? leadingDistribution,
    double? leading,
    ui.FontWeight? fontWeight,
    ui.FontStyle? fontStyle,
    bool? forceStrutHeight,
  })  : _fontFamily = fontFamily,
        _fontFamilyFallback = fontFamilyFallback,
        _fontSize = fontSize,
        _height = height,
        _leadingDistribution = leadingDistribution,
        _leading = leading,
        _fontWeight = fontWeight,
        _fontStyle = fontStyle,
        _forceStrutHeight = forceStrutHeight;

  final String? _fontFamily;
  final List<String>? _fontFamilyFallback;
  final double? _fontSize;
  final double? _height;
  final double? _leading;
  final ui.FontWeight? _fontWeight;
  final ui.FontStyle? _fontStyle;
  final bool? _forceStrutHeight;
  final ui.TextLeadingDistribution? _leadingDistribution;

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) {
      return true;
    }
    if (other.runtimeType != runtimeType) {
      return false;
    }
    return other is EngineStrutStyle &&
        other._fontFamily == _fontFamily &&
        other._fontSize == _fontSize &&
        other._height == _height &&
        other._leading == _leading &&
        other._leadingDistribution == _leadingDistribution &&
        other._fontWeight == _fontWeight &&
        other._fontStyle == _fontStyle &&
        other._forceStrutHeight == _forceStrutHeight &&
        listEquals<String>(other._fontFamilyFallback, _fontFamilyFallback);
  }

  @override
  int get hashCode => Object.hash(
        _fontFamily,
        _fontFamilyFallback,
        _fontSize,
        _height,
        _leading,
        _leadingDistribution,
        _fontWeight,
        _fontStyle,
        _forceStrutHeight,
      );
}

/// Holds information for a placeholder in a paragraph.
///
/// [width], [height] and [baselineOffset] are expected to be already scaled.
class ParagraphPlaceholder {
  ParagraphPlaceholder(
    this.width,
    this.height,
    this.alignment, {
    required this.baselineOffset,
    required this.baseline,
  });

  /// The scaled width of the placeholder.
  final double width;

  /// The scaled height of the placeholder.
  final double height;

  /// Specifies how the placeholder rectangle will be vertically aligned with
  /// the surrounding text.
  final ui.PlaceholderAlignment alignment;

  /// When the [alignment] value is [ui.PlaceholderAlignment.baseline], the
  /// [baselineOffset] indicates the distance from the baseline to the top of
  /// the placeholder rectangle.
  final double baselineOffset;

  /// Dictates whether to use alphabetic or ideographic baseline.
  final ui.TextBaseline baseline;
}

/// Converts [fontWeight] to its CSS equivalent value.
String? fontWeightToCss(ui.FontWeight? fontWeight) {
  if (fontWeight == null) {
    return null;
  }
  return fontWeightIndexToCss(fontWeightIndex: fontWeight.index);
}

String fontWeightIndexToCss({int fontWeightIndex = 3}) {
  switch (fontWeightIndex) {
    case 0:
      return '100';
    case 1:
      return '200';
    case 2:
      return '300';
    case 3:
      return 'normal';
    case 4:
      return '500';
    case 5:
      return '600';
    case 6:
      return 'bold';
    case 7:
      return '800';
    case 8:
      return '900';
  }

  assert(() {
    throw AssertionError(
      'Failed to convert font weight $fontWeightIndex to CSS.',
    );
  }());

  return '';
}

/// Applies a text [style] to an [element], translating the properties to their
/// corresponding CSS equivalents.
///
/// If [isSpan] is true, the text element is a span within richtext and
/// should not assign effectiveFontFamily if fontFamily was not specified.
void applyTextStyleToElement({
  required DomHTMLElement element,
  required EngineTextStyle style,
  bool isSpan = false,
}) {
  assert(element != null); // ignore: unnecessary_null_comparison
  assert(style != null); // ignore: unnecessary_null_comparison
  bool updateDecoration = false;
  final DomCSSStyleDeclaration cssStyle = element.style;

  final ui.Color? color = style.foreground?.color ?? style.color;
  if (style.foreground?.style == ui.PaintingStyle.stroke) {
    // When comparing the outputs of the Bitmap Canvas and the DOM
    // implementation, we have found, that we need to set the background color
    // of the text to transparent to achieve the same effect as in the Bitmap
    // Canvas and the Skia Engine where only the text stroke is painted.
    // If we don't set it here to transparent, the text will inherit the color
    // of it's parent element.
    cssStyle.color = 'transparent';
    // Use hairline (device pixel when strokeWidth is not specified).
    final double? strokeWidth = style.foreground?.strokeWidth;
    final double adaptedWidth = strokeWidth != null && strokeWidth > 0
        ? strokeWidth
        : 1.0 / ui.window.devicePixelRatio;
    cssStyle.textStroke = '${adaptedWidth}px ${colorToCssString(color)}';
  } else if (color != null) {
    cssStyle.color = colorToCssString(color)!;
  }
  final ui.Color? background = style.background?.color;
  if (background != null) {
    cssStyle.backgroundColor = colorToCssString(background)!;
  }
  final double? fontSize = style.fontSize;
  if (fontSize != null) {
    cssStyle.fontSize = '${fontSize.floor()}px';
  }
  if (style.fontWeight != null) {
    cssStyle.fontWeight = fontWeightToCss(style.fontWeight)!;
  }
  if (style.fontStyle != null) {
    cssStyle.fontStyle =
        style.fontStyle == ui.FontStyle.normal ? 'normal' : 'italic';
  }
  // For test environment use effectiveFontFamily since we need to
  // consistently use Ahem font.
  if (isSpan && !ui.debugEmulateFlutterTesterEnvironment) {
    cssStyle.fontFamily = canonicalizeFontFamily(style.fontFamily)!;
  } else {
    cssStyle.fontFamily = canonicalizeFontFamily(style.effectiveFontFamily)!;
  }
  if (style.letterSpacing != null) {
    cssStyle.letterSpacing = '${style.letterSpacing}px';
  }
  if (style.wordSpacing != null) {
    cssStyle.wordSpacing = '${style.wordSpacing}px';
  }
  if (style.decoration != null) {
    updateDecoration = true;
  }
  final List<ui.Shadow>? shadows = style.shadows;
  if (shadows != null) {
    cssStyle.textShadow = _shadowListToCss(shadows);
  }

  if (updateDecoration) {
    if (style.decoration != null) {
      final String? textDecoration =
          _textDecorationToCssString(style.decoration, style.decorationStyle);
      if (textDecoration != null) {
        if (browserEngine == BrowserEngine.webkit) {
          setElementStyle(element, '-webkit-text-decoration', textDecoration);
        } else {
          cssStyle.textDecoration = textDecoration;
        }
        final ui.Color? decorationColor = style.decorationColor;
        if (decorationColor != null) {
          cssStyle.textDecorationColor = colorToCssString(decorationColor)!;
        }
      }
    }
  }

  final List<ui.FontFeature>? fontFeatures = style.fontFeatures;
  if (fontFeatures != null && fontFeatures.isNotEmpty) {
    cssStyle.fontFeatureSettings = _fontFeatureListToCss(fontFeatures);
  }

  final List<ui.FontVariation>? fontVariations = style.fontVariations;
  if (fontVariations != null && fontVariations.isNotEmpty) {
    cssStyle.setProperty(
        'font-variation-settings', _fontVariationListToCss(fontVariations));
  }
}

String _shadowListToCss(List<ui.Shadow> shadows) {
  if (shadows.isEmpty) {
    return '';
  }
  // CSS text-shadow is a comma separated list of shadows.
  // <offsetx> <offsety> <blur-radius> <color>.
  // Shadows are applied front-to-back with first shadow on top.
  // Color is optional. offsetx,y are required. blur-radius is optional as well
  // and defaults to 0.
  final StringBuffer sb = StringBuffer();
  final int len = shadows.length;
  for (int i = 0; i < len; i++) {
    if (i != 0) {
      sb.write(',');
    }
    final ui.Shadow shadow = shadows[i];
    sb.write('${shadow.offset.dx}px ${shadow.offset.dy}px '
        '${shadow.blurRadius}px ${colorToCssString(shadow.color)}');
  }
  return sb.toString();
}

String _fontFeatureListToCss(List<ui.FontFeature> fontFeatures) {
  assert(fontFeatures.isNotEmpty);

  // For more details, see:
  // * https://developer.mozilla.org/en-US/docs/Web/CSS/font-feature-settings
  final StringBuffer sb = StringBuffer();
  final int len = fontFeatures.length;
  for (int i = 0; i < len; i++) {
    if (i != 0) {
      sb.write(',');
    }
    final ui.FontFeature fontFeature = fontFeatures[i];
    sb.write('"${fontFeature.feature}" ${fontFeature.value}');
  }
  return sb.toString();
}

String _fontVariationListToCss(List<ui.FontVariation> fontVariations) {
  assert(fontVariations.isNotEmpty);

  final StringBuffer sb = StringBuffer();
  final int len = fontVariations.length;
  for (int i = 0; i < len; i++) {
    if (i != 0) {
      sb.write(',');
    }
    final ui.FontVariation fontVariation = fontVariations[i];
    sb.write('"${fontVariation.axis}" ${fontVariation.value}');
  }
  return sb.toString();
}

/// Converts text decoration style to CSS text-decoration-style value.
String? _textDecorationToCssString(
    ui.TextDecoration? decoration, ui.TextDecorationStyle? decorationStyle) {
  final StringBuffer decorations = StringBuffer();
  if (decoration != null) {
    if (decoration.contains(ui.TextDecoration.underline)) {
      decorations.write('underline ');
    }
    if (decoration.contains(ui.TextDecoration.overline)) {
      decorations.write('overline ');
    }
    if (decoration.contains(ui.TextDecoration.lineThrough)) {
      decorations.write('line-through ');
    }
  }
  if (decorationStyle != null) {
    decorations.write(_decorationStyleToCssString(decorationStyle));
  }
  return decorations.isEmpty ? null : decorations.toString();
}

String? _decorationStyleToCssString(ui.TextDecorationStyle decorationStyle) {
  switch (decorationStyle) {
    case ui.TextDecorationStyle.dashed:
      return 'dashed';
    case ui.TextDecorationStyle.dotted:
      return 'dotted';
    case ui.TextDecorationStyle.double:
      return 'double';
    case ui.TextDecorationStyle.solid:
      return 'solid';
    case ui.TextDecorationStyle.wavy:
      return 'wavy';
    default:
      return null;
  }
}

/// Converts [align] to its corresponding CSS value.
///
/// This value is used as the "text-align" CSS property, e.g.:
///
/// ```css
/// text-align: right;
/// ```
String textAlignToCssValue(
    ui.TextAlign? align, ui.TextDirection textDirection) {
  switch (align) {
    case ui.TextAlign.left:
      return 'left';
    case ui.TextAlign.right:
      return 'right';
    case ui.TextAlign.center:
      return 'center';
    case ui.TextAlign.justify:
      return 'justify';
    case ui.TextAlign.end:
      switch (textDirection) {
        case ui.TextDirection.ltr:
          return 'end';
        case ui.TextDirection.rtl:
          return 'left';
      }
    case ui.TextAlign.start:
      switch (textDirection) {
        case ui.TextDirection.ltr:
          return ''; // it's the default
        case ui.TextDirection.rtl:
          return 'right';
      }
    case null:
      // If align is not specified return default.
      return '';
  }
}
