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.





/// Converts [matrix] to CSS transform value.
String matrix4ToCssTransform(Matrix4 matrix) {
  return float64ListToCssTransform(matrix.storage);
}

/// Converts [matrix] to CSS transform value.
///
/// To avoid blurry text on some screens this function uses a 2D CSS transform
/// if it detects that [matrix] is a 2D transform. Otherwise, it uses a 3D CSS
/// transform.
///
/// See also:
///  * https://github.com/flutter/flutter/issues/32274
///  * https://bugs.chromium.org/p/chromium/issues/detail?id=1040222
String float64ListToCssTransform(List<double> matrix) {
  assert(matrix.length == 16);
  final TransformKind transformKind = transformKindOf(matrix);
  if (transformKind == TransformKind.transform2d) {
    return float64ListToCssTransform2d(matrix);
  } else if (transformKind == TransformKind.complex) {
    return float64ListToCssTransform3d(matrix);
  } else {
    assert(transformKind == TransformKind.identity);
    return 'none';
  }
}

/// The kind of effect a transform matrix performs.
enum TransformKind {
  /// No effect.
  ///
  /// We do not want to set any CSS properties in this case.
  identity,

  /// A transform that contains only 2d scale, rotation, and translation.
  ///
  /// We prefer to use "matrix" instead of "matrix3d" in this case.
  transform2d,

  /// All other kinds of transforms.
  ///
  /// In this case we will use "matrix3d".
  complex,
}

/// Detects the kind of transform the [matrix] performs.
TransformKind transformKindOf(List<double> matrix) {
  assert(matrix.length == 16);
  final List<double> m = matrix;

  // If matrix contains scaling, rotation, z translation or
  // perspective transform, it is not considered simple.
  final bool isSimple2dTransform =
      m[15] ==
          1.0 && // start reading from the last element to eliminate range checks in subsequent reads.
      m[14] == 0.0 && // z translation is NOT simple
      // m[13] - y translation is simple
      // m[12] - x translation is simple
      m[11] == 0.0 &&
      m[10] == 1.0 &&
      m[9] == 0.0 &&
      m[8] == 0.0 &&
      m[7] == 0.0 &&
      m[6] == 0.0 &&
      // m[5] - scale y is simple
      // m[4] - 2D rotation is simple
      m[3] == 0.0 &&
      m[2] == 0.0;
  // m[1] - 2D rotation is simple
  // m[0] - scale x is simple

  if (!isSimple2dTransform) {
    return TransformKind.complex;
  }

  // From this point on we're sure the transform is 2D, but we don't know if
  // it's identity or not. To check, we need to look at the remaining elements
  // that were not checked above.
  final bool isIdentityTransform =
      m[0] == 1.0 && m[1] == 0.0 && m[4] == 0.0 && m[5] == 1.0 && m[12] == 0.0 && m[13] == 0.0;

  if (isIdentityTransform) {
    return TransformKind.identity;
  } else {
    return TransformKind.transform2d;
  }
}

/// Returns `true` is the [matrix] describes an identity transformation.
bool isIdentityFloat32ListTransform(Float32List matrix) {
  assert(matrix.length == 16);
  return transformKindOf(matrix) == TransformKind.identity;
}

/// Converts [matrix] to CSS transform 2D matrix value.
///
/// The [matrix] must not be a [TransformKind.complex] transform, because CSS
/// `matrix` can only express 2D transforms. [TransformKind.identity] is
/// permitted. However, it is inefficient to construct a matrix for an identity
/// transform. Consider removing the CSS `transform` property from elements
/// that apply identity transform.
String float64ListToCssTransform2d(List<double> matrix) {
  assert(transformKindOf(matrix) != TransformKind.complex);
  return 'matrix(${matrix[0]},${matrix[1]},${matrix[4]},${matrix[5]},${matrix[12]},${matrix[13]})';
}

/// Converts [matrix] to a 3D CSS transform value.
String float64ListToCssTransform3d(List<double> matrix) {
  assert(matrix.length == 16);
  final List<double> m = matrix;
  if (m[0] == 1.0 &&
      m[1] == 0.0 &&
      m[2] == 0.0 &&
      m[3] == 0.0 &&
      m[4] == 0.0 &&
      m[5] == 1.0 &&
      m[6] == 0.0 &&
      m[7] == 0.0 &&
      m[8] == 0.0 &&
      m[9] == 0.0 &&
      m[10] == 1.0 &&
      m[11] == 0.0 &&
      // 12 can be anything
      // 13 can be anything
      m[14] == 0.0 &&
      m[15] == 1.0) {
    final double tx = m[12];
    final double ty = m[13];
    return 'translate3d(${tx}px, ${ty}px, 0px)';
  } else {
    return 'matrix3d(${m[0]},${m[1]},${m[2]},${m[3]},${m[4]},${m[5]},${m[6]},${m[7]},${m[8]},${m[9]},${m[10]},${m[11]},${m[12]},${m[13]},${m[14]},${m[15]})';
  }
}

final Float32List _tempRectData = Float32List(4);

/// Transforms a [ui.Rect] given the effective [transform].
///
/// The resulting rect is aligned to the pixel grid, i.e. two of
/// its sides are vertical and two are horizontal. In the presence of rotations
/// the rectangle is inflated such that it fits the rotated rectangle.
ui.Rect transformRectWithMatrix(Matrix4 transform, ui.Rect rect) {
  _tempRectData[0] = rect.left;
  _tempRectData[1] = rect.top;
  _tempRectData[2] = rect.right;
  _tempRectData[3] = rect.bottom;
  transformLTRB(transform, _tempRectData);
  return ui.Rect.fromLTRB(_tempRectData[0], _tempRectData[1], _tempRectData[2], _tempRectData[3]);
}

/// Temporary storage for intermediate data used by [transformLTRB].
///
/// WARNING: do not use this outside [transformLTRB]. Sharing this variable in
/// other contexts will lead to bugs.
final Float32List _tempPointData = Float32List(16);
final Matrix4 _tempPointMatrix = Matrix4.fromFloat32List(_tempPointData);

/// Transforms a rectangle given the effective [transform].
///
/// This is the same as [transformRect], except that the rect is specified
/// in terms of left, top, right, and bottom edge offsets.
void transformLTRB(Matrix4 transform, Float32List ltrb) {
  // Construct a matrix where each row represents a vector pointing at
  // one of the four corners of the (left, top, right, bottom) rectangle.
  // Using the row-major order allows us to multiply the matrix in-place
  // by the transposed current transformation matrix. The vector_math
  // library has a convenience function `multiplyTranspose` that performs
  // the multiplication without copying. This way we compute the positions
  // of all four points in a single matrix-by-matrix multiplication at the
  // cost of one `Matrix4` instance and one `Float32List` instance.
  //
  // The rejected alternative was to use `Vector3` for each point and
  // multiply by the current transform. However, that would cost us four
  // `Vector3` instances, four `Float32List` instances, and four
  // matrix-by-vector multiplications.
  //
  // `Float32List` initializes the array with zeros, so we do not have to
  // fill in every single element.

  // Row 0: top-left
  _tempPointData[0] = ltrb[0];
  _tempPointData[4] = ltrb[1];
  _tempPointData[8] = 0;
  _tempPointData[12] = 1;

  // Row 1: top-right
  _tempPointData[1] = ltrb[2];
  _tempPointData[5] = ltrb[1];
  _tempPointData[9] = 0;
  _tempPointData[13] = 1;

  // Row 2: bottom-left
  _tempPointData[2] = ltrb[0];
  _tempPointData[6] = ltrb[3];
  _tempPointData[10] = 0;
  _tempPointData[14] = 1;

  // Row 3: bottom-right
  _tempPointData[3] = ltrb[2];
  _tempPointData[7] = ltrb[3];
  _tempPointData[11] = 0;
  _tempPointData[15] = 1;

  _tempPointMatrix.multiplyTranspose(transform);

  // Handle non-homogenous matrices.
  double w = transform[15];
  if (w == 0.0) {
    w = 1.0;
  }

  ltrb[0] =
      math.min(
        math.min(math.min(_tempPointData[0], _tempPointData[1]), _tempPointData[2]),
        _tempPointData[3],
      ) /
      w;
  ltrb[1] =
      math.min(
        math.min(math.min(_tempPointData[4], _tempPointData[5]), _tempPointData[6]),
        _tempPointData[7],
      ) /
      w;
  ltrb[2] =
      math.max(
        math.max(math.max(_tempPointData[0], _tempPointData[1]), _tempPointData[2]),
        _tempPointData[3],
      ) /
      w;
  ltrb[3] =
      math.max(
        math.max(math.max(_tempPointData[4], _tempPointData[5]), _tempPointData[6]),
        _tempPointData[7],
      ) /
      w;
}

/// Returns true if [rect] contains every point that is also contained by the
/// [other] rect.
///
/// Points on the edges of both rectangles are also considered. For example,
/// this returns true when the two rects are equal to each other.
bool rectContainsOther(ui.Rect rect, ui.Rect other) {
  return rect.left <= other.left &&
      rect.top <= other.top &&
      rect.right >= other.right &&
      rect.bottom >= other.bottom;
}

extension CssColor on ui.Color {
  /// Converts color to a css compatible attribute value.
  String toCssString() {
    return colorValueToCssString(value);
  }
}

// Converts a color value (as an int) into a CSS-compatible value.
String colorValueToCssString(int value) {
  if (value == 0xFF000000) {
    return '#000000';
  }
  if ((0xff000000 & value) == 0xff000000) {
    final String hexValue = (value & 0xFFFFFF).toRadixString(16);
    final int hexValueLength = hexValue.length;
    return switch (hexValueLength) {
      1 => '#00000$hexValue',
      2 => '#0000$hexValue',
      3 => '#000$hexValue',
      4 => '#00$hexValue',
      5 => '#0$hexValue',
      _ => '#$hexValue',
    };
  } else {
    final double alpha = ((value >> 24) & 0xFF) / 255.0;
    final StringBuffer sb = StringBuffer();
    sb.write('rgba(');
    sb.write(((value >> 16) & 0xFF).toString());
    sb.write(',');
    sb.write(((value >> 8) & 0xFF).toString());
    sb.write(',');
    sb.write((value & 0xFF).toString());
    sb.write(',');
    sb.write(alpha.toString());
    sb.write(')');
    return sb.toString();
  }
}

/// From: https://developer.mozilla.org/en-US/docs/Web/CSS/font-family#Syntax
///
/// Generic font families are a fallback mechanism, a means of preserving some
/// of the style sheet author's intent when none of the specified fonts are
/// available. Generic family names are keywords and must not be quoted. A
/// generic font family should be the last item in the list of font family
/// names.
const Set<String> _genericFontFamilies = <String>{
  'serif',
  'sans-serif',
  'monospace',
  'cursive',
  'fantasy',
  'system-ui',
  'math',
  'emoji',
  'fangsong',
};

/// A default fallback font family in case an unloaded font has been requested.
///
/// -apple-system targets San Francisco in Safari (on Mac OS X and iOS),
/// and it targets Neue Helvetica and Lucida Grande on older versions of
/// Mac OS X. It properly selects between San Francisco Text and
/// San Francisco Display depending on the text’s size.
///
/// For iOS, default to -apple-system, where it should be available, otherwise
/// default to Arial. BlinkMacSystemFont is used for Chrome on iOS.
String get _fallbackFontFamily {
  if (isIOS15) {
    // Remove the "-apple-system" fallback font because it causes a crash in
    // iOS 15.
    //
    // See github issue: https://github.com/flutter/flutter/issues/90705
    // See webkit bug: https://bugs.webkit.org/show_bug.cgi?id=231686
    return 'BlinkMacSystemFont';
  }
  if (isMacOrIOS) {
    return '-apple-system, BlinkMacSystemFont';
  }
  return 'Arial';
}

/// Create a font-family string appropriate for CSS.
///
/// If the given [fontFamily] is a generic font-family, then just return it.
/// Otherwise, wrap the family name in quotes and add a fallback font family.
String? canonicalizeFontFamily(String? fontFamily) {
  if (_genericFontFamilies.contains(fontFamily)) {
    return fontFamily;
  }
  if (isMacOrIOS) {
    // Unlike Safari, Chrome on iOS does not correctly fallback to cupertino
    // on sans-serif.
    // Map to San Francisco Text/Display fonts, use -apple-system,
    // BlinkMacSystemFont.
    if (fontFamily == '.SF Pro Text' ||
        fontFamily == '.SF Pro Display' ||
        fontFamily == '.SF UI Text' ||
        fontFamily == '.SF UI Display') {
      return _fallbackFontFamily;
    }
  }
  return '"$fontFamily", $_fallbackFontFamily, sans-serif';
}

/// Converts a list of [Offset] to a typed array of floats.
Float32List offsetListToFloat32List(List<ui.Offset> offsetList) {
  final int length = offsetList.length;
  final Float32List floatList = Float32List(length * 2);
  for (int i = 0, destIndex = 0; i < length; i++, destIndex += 2) {
    floatList[destIndex] = offsetList[i].dx;
    floatList[destIndex + 1] = offsetList[i].dy;
  }
  return floatList;
}

/// Prints a warning message to the console.
///
/// This function can be overridden in tests. This could be useful, for example,
/// to verify that warnings are printed under certain circumstances.
void Function(String) printWarning = domWindow.console.warn;

/// Determines if lists [a] and [b] are deep equivalent.
///
/// Returns true if the lists are both null, or if they are both non-null, have
/// the same length, and contain the same elements in the same order. Returns
/// false otherwise.
bool listEquals<T>(List<T>? a, List<T>? b) {
  if (a == null) {
    return b == null;
  }
  if (b == null || a.length != b.length) {
    return false;
  }
  for (int index = 0; index < a.length; index += 1) {
    if (a[index] != b[index]) {
      return false;
    }
  }
  return true;
}

/// Determines if lists [a] and [b] are deep equivalent, regardless of their
/// order.
///
/// Returns true if the lists are both null, or if they are both non-null, have
/// the same length, and contain the same elements regardless of their order.
/// Returns false otherwise.
bool unorderedListEqual<T>(List<T>? a, List<T>? b) {
  if (a == b) {
    return true;
  }
  if ((a?.isEmpty ?? true) && (b?.isEmpty ?? true)) {
    return true;
  }

  if ((a == null) != (b == null)) {
    return false;
  }
  // They most both be non-null now, and at least one of them is not empty.
  if (a!.length != b!.length) {
    return false;
  }

  if (a.length == 1) {
    return a.first == b.first;
  }

  if (a.length == 2) {
    return (a.first == b.first && a.last == b.last) || (a.last == b.first && a.first == b.last);
  }

  // Complex cases.
  final Map<T, int> wordCounts = <T, int>{};
  for (final T word in a) {
    final int count = wordCounts[word] ?? 0;
    wordCounts[word] = count + 1;
  }

  for (final T otherWord in b) {
    final int? count = wordCounts[otherWord];
    if (count == null || count == 0) {
      return false;
    }
    if (count == 1) {
      wordCounts.remove(otherWord);
    } else {
      wordCounts[otherWord] = count - 1;
    }
  }
  return wordCounts.isEmpty;
}

bool paintEquals(ui.Paint? a, ui.Paint? b) {
  if (identical(a, b)) {
    // They are both the same instance or both null.
    return true;
  }
  if (a == null || b == null) {
    return false;
  }
  return a.blendMode == b.blendMode &&
      a.color == b.color &&
      a.colorFilter == b.colorFilter &&
      a.filterQuality == b.filterQuality &&
      a.imageFilter == b.imageFilter &&
      a.invertColors == b.invertColors &&
      a.isAntiAlias == b.isAntiAlias &&
      a.maskFilter == b.maskFilter &&
      a.shader == b.shader &&
      a.strokeCap == b.strokeCap &&
      a.strokeJoin == b.strokeJoin &&
      a.strokeMiterLimit == b.strokeMiterLimit &&
      a.strokeWidth == b.strokeWidth &&
      a.style == b.style;
}

/// Extensions to [Map] that make it easier to treat it as a JSON object. The
/// keys are `dynamic` because when JSON is deserialized from method channels
/// it arrives as `Map<dynamic, dynamic>`.
// TODO(yjbanov): use Json typedef when type aliases are shipped
extension JsonExtensions on Map<dynamic, dynamic> {
  Map<String, dynamic> readJson(String propertyName) {
    return this[propertyName] as Map<String, dynamic>;
  }

  Map<String, dynamic>? tryJson(String propertyName) {
    return this[propertyName] as Map<String, dynamic>?;
  }

  Map<dynamic, dynamic> readDynamicJson(String propertyName) {
    return this[propertyName] as Map<dynamic, dynamic>;
  }

  Map<dynamic, dynamic>? tryDynamicJson(String propertyName) {
    return this[propertyName] as Map<dynamic, dynamic>?;
  }

  List<dynamic> readList(String propertyName) {
    return this[propertyName] as List<dynamic>;
  }

  List<dynamic>? tryList(String propertyName) {
    return this[propertyName] as List<dynamic>?;
  }

  List<T> castList<T>(String propertyName) {
    return (this[propertyName] as List<dynamic>).cast<T>();
  }

  List<T>? tryCastList<T>(String propertyName) {
    final List<dynamic>? rawList = tryList(propertyName);
    if (rawList == null) {
      return null;
    }
    return rawList.cast<T>();
  }

  String readString(String propertyName) {
    return this[propertyName] as String;
  }

  String? tryString(String propertyName) {
    return this[propertyName] as String?;
  }

  bool readBool(String propertyName) {
    return this[propertyName] as bool;
  }

  bool? tryBool(String propertyName) {
    return this[propertyName] as bool?;
  }

  int readInt(String propertyName) {
    return (this[propertyName] as num).toInt();
  }

  int? tryInt(String propertyName) {
    return (this[propertyName] as num?)?.toInt();
  }

  double readDouble(String propertyName) {
    return (this[propertyName] as num).toDouble();
  }

  double? tryDouble(String propertyName) {
    return (this[propertyName] as num?)?.toDouble();
  }
}

/// Prints a list of bytes in hex format.
///
/// Bytes are separated by one space and are padded on the left to always show
/// two digits.
///
/// Example:
///
///     Input: [0, 1, 2, 3]
///     Output: 0x00 0x01 0x02 0x03
String bytesToHexString(List<int> data) {
  return data.map((int byte) => '0x${byte.toRadixString(16).padLeft(2, '0')}').join(' ');
}

/// Sets a style property on [element].
///
/// [name] is the name of the property. [value] is the value of the property.
/// If [value] is null, removes the style property.
void setElementStyle(DomElement element, String name, String? value) {
  if (value == null) {
    element.style.removeProperty(name);
  } else {
    element.style.setProperty(name, value);
  }
}

void setThemeColor(ui.Color? color) {
  DomHTMLMetaElement? theme = domDocument.querySelector('#flutterweb-theme') as DomHTMLMetaElement?;

  if (color != null) {
    if (theme == null) {
      theme = createDomHTMLMetaElement()
        ..id = 'flutterweb-theme'
        ..name = 'theme-color';
      domDocument.head!.append(theme);
    }
    theme.content = color.toCssString();
  } else {
    theme?.remove();
  }
}

/// Ensure a "meta" tag with [name] and [content] is set on the page.
void ensureMetaTag(String name, String content) {
  final DomElement? existingTag = domDocument.querySelector('meta[name=$name][content=$content]');

  if (existingTag == null) {
    final DomHTMLMetaElement meta = createDomHTMLMetaElement()
      ..name = name
      ..content = content;
    domDocument.head!.append(meta);
  }
}

/// A helper that finds an element in an iterable that satisfy a predicate, or
/// returns null otherwise.
///
/// This is mostly useful for iterables containing non-null elements.
extension FirstWhereOrNull<T> on Iterable<T> {
  T? firstWhereOrNull(bool Function(T element) test) {
    for (final T element in this) {
      if (test(element)) {
        return element;
      }
    }
    return null;
  }
}

typedef _LruCacheEntry<K extends Object, V extends Object> = ({K key, V value});

/// Caches up to a [maximumSize] key-value pairs.
///
/// Call [cache] to cache a key-value pair.
class LruCache<K extends Object, V extends Object> {
  LruCache(this.maximumSize);

  /// The maximum number of key/value pairs this cache can contain.
  ///
  /// To avoid exceeding this limit the cache remove least recently used items.
  final int maximumSize;

  /// A doubly linked list of the objects in the cache.
  ///
  /// This makes it fast to move a recently used object to the front.
  final DoubleLinkedQueue<_LruCacheEntry<K, V>> _itemQueue =
      DoubleLinkedQueue<_LruCacheEntry<K, V>>();

  
  DoubleLinkedQueue<_LruCacheEntry<K, V>> get debugItemQueue => _itemQueue;

  /// A map of objects to their associated node in the [_itemQueue].
  ///
  /// This makes it fast to find the node in the queue when we need to
  /// move the object to the front of the queue.
  final Map<K, DoubleLinkedQueueEntry<_LruCacheEntry<K, V>>> _itemMap =
      <K, DoubleLinkedQueueEntry<_LruCacheEntry<K, V>>>{};

  
  Map<K, DoubleLinkedQueueEntry<_LruCacheEntry<K, V>>> get itemMap => _itemMap;

  /// The number of objects in the cache.
  int get length => _itemQueue.length;

  /// Whether or not [object] is in the cache.
  ///
  /// This is only for testing.
  
  bool debugContainsValue(V object) {
    return _itemMap.containsValue(object);
  }

  
  bool debugContainsKey(K key) {
    return _itemMap.containsKey(key);
  }

  /// Returns the cached value associated with the [key].
  ///
  /// If the value is not in the cache, returns null.
  V? operator [](K key) {
    return _itemMap[key]?.element.value;
  }

  /// Caches the given [key]/[value] pair in this cache.
  ///
  /// If the pair is not already in the cache, adds it to the cache as the most
  /// recently used pair.
  ///
  /// If the [key] is already in the cache, moves it to the most recently used
  /// position. If the [value] corresponding to the [key] is different from
  /// what's in the cache, updates the value.
  void cache(K key, V value) {
    final DoubleLinkedQueueEntry<_LruCacheEntry<K, V>>? item = _itemMap[key];
    if (item == null) {
      // New key-value pair, just add.
      _add(key, value);
    } else if (item.element.value != value) {
      // Key already in the cache, but value is new. Re-add.
      item.remove();
      _add(key, value);
    } else {
      // Key-value pair already in the cache, move to most recently used.
      item.remove();
      _itemQueue.addFirst(item.element);
      _itemMap[key] = _itemQueue.firstEntry()!;
    }
  }

  void clear() {
    _itemQueue.clear();
    _itemMap.clear();
  }

  void _add(K key, V value) {
    _itemQueue.addFirst((key: key, value: value));
    _itemMap[key] = _itemQueue.firstEntry()!;

    if (_itemQueue.length > maximumSize) {
      _removeLeastRecentlyUsedValue();
    }
  }

  void _removeLeastRecentlyUsedValue() {
    final bool didRemove = _itemMap.remove(_itemQueue.last.key) != null;
    assert(didRemove);
    _itemQueue.removeLast();
  }
}

/// Returns the VM-compatible string for the tile mode.
String tileModeString(ui.TileMode? tileMode) => tileMode?.name ?? 'unspecified';

/// A size where both the width and height are integers.
class BitmapSize {
  const BitmapSize(this.width, this.height);

  /// Returns a [BitmapSize] by rounding the width and height of a [ui.Size] to
  /// the nearest integer.
  BitmapSize.fromSize(ui.Size size) : width = size.width.round(), height = size.height.round();

  final int width;
  final int height;

  @override
  bool operator ==(Object other) {
    return other is BitmapSize && other.width == width && other.height == height;
  }

  @override
  int get hashCode => Object.hash(width, height);

  @override
  String toString() => 'BitmapSize($width, $height)';

  ui.Size toSize() {
    return ui.Size(width.toDouble(), height.toDouble());
  }

  bool get isEmpty => width == 0 || height == 0;

  static const BitmapSize zero = BitmapSize(0, 0);
}

String _generateDebugFilename(String filePrefix) {
  final now = DateTime.now();
  final String y = now.year.toString().padLeft(4, '0');
  final String mo = now.month.toString().padLeft(2, '0');
  final String d = now.day.toString().padLeft(2, '0');
  final String h = now.hour.toString().padLeft(2, '0');
  final String mi = now.minute.toString().padLeft(2, '0');
  final String s = now.second.toString().padLeft(2, '0');
  return '$filePrefix-$y-$mo-$d-$h-$mi-$s.json';
}

void downloadDebugInfo(String filePrefix, Map<String, dynamic> json) {
  final String jsonString = const JsonEncoder.withIndent(' ').convert(json);
  final blob = createDomBlob([jsonString], {'type': 'application/json'});
  final url = domWindow.URL.createObjectURL(blob);
  final element = domDocument.createElement('a');
  element.setAttribute('href', url);
  element.setAttribute('download', _generateDebugFilename(filePrefix));
  element.click();
}
