[web] Use greatest span font size on the parent paragraph (#25513)

diff --git a/lib/web_ui/dev/goldens_lock.yaml b/lib/web_ui/dev/goldens_lock.yaml
index d081079..f04236f 100644
--- a/lib/web_ui/dev/goldens_lock.yaml
+++ b/lib/web_ui/dev/goldens_lock.yaml
@@ -1,2 +1,2 @@
 repository: https://github.com/flutter/goldens.git
-revision: 82c1ccb1247f279cfd9f23a3929d13e2f32771ad
+revision: f868ee6b6faef13469498e9b160b45fed9fdc567
diff --git a/lib/web_ui/lib/src/engine/text/canvas_paragraph.dart b/lib/web_ui/lib/src/engine/text/canvas_paragraph.dart
index ce9e131..2f626de 100644
--- a/lib/web_ui/lib/src/engine/text/canvas_paragraph.dart
+++ b/lib/web_ui/lib/src/engine/text/canvas_paragraph.dart
@@ -134,6 +134,7 @@
 
     // 1. Set paragraph-level styles.
     _applyNecessaryParagraphStyles(element: rootElement, style: paragraphStyle);
+    _applySpanStylesToParagraph(element: rootElement, spans: spans);
     final html.CssStyleDeclaration cssStyle = rootElement.style;
     cssStyle
       ..position = 'absolute'
@@ -284,6 +285,39 @@
   }
 }
 
+/// Applies some span-level style to a paragraph [element].
+///
+/// For example, it looks for the greatest font size among spans, and applies it
+/// to the paragraph. While this seems to have no effect, it prevents the
+/// paragraph from inheriting its font size from the body tag, which leads to
+/// incorrect vertical alignment of spans.
+void _applySpanStylesToParagraph({
+  required html.HtmlElement element,
+  required List<ParagraphSpan> spans,
+}) {
+  double fontSize = 0.0;
+  String? fontFamily;
+  for (final ParagraphSpan span in spans) {
+    if (span is FlatTextSpan) {
+      final double? spanFontSize = span.style._fontSize;
+      if (spanFontSize != null && spanFontSize > fontSize) {
+        fontSize = spanFontSize;
+        if (span.style._isFontFamilyProvided) {
+          fontFamily = span.style._effectiveFontFamily;
+        }
+      }
+    }
+  }
+
+  final html.CssStyleDeclaration cssStyle = element.style;
+  if (fontSize != 0.0) {
+    cssStyle.fontSize = '${fontSize}px';
+  }
+  if (fontFamily != null) {
+    cssStyle.fontFamily = canonicalizeFontFamily(fontFamily);
+  }
+}
+
 /// A common interface for all types of spans that make up a paragraph.
 ///
 /// These spans are stored as a flat list in the paragraph object.
diff --git a/lib/web_ui/test/golden_tests/engine/canvas_paragraph/general_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_paragraph/general_test.dart
index 73a97de..d86a7fd 100644
--- a/lib/web_ui/test/golden_tests/engine/canvas_paragraph/general_test.dart
+++ b/lib/web_ui/test/golden_tests/engine/canvas_paragraph/general_test.dart
@@ -4,6 +4,7 @@
 
 // @dart = 2.6
 import 'dart:async';
+import 'dart:html' as html;
 import 'dart:math' as math;
 
 import 'package:test/bootstrap/browser.dart';
@@ -226,6 +227,63 @@
     return takeScreenshot(canvas, bounds, 'canvas_paragraph_giant_paragraph_style_dom');
   });
 
+  test('giant font size on the body tag (DOM)', () async {
+    const Rect bounds = Rect.fromLTWH(0, 0, 600, 200);
+
+    // Store the old font size value on the body, and set a gaint font size.
+    final String oldBodyFontSize = html.document.body.style.fontSize;
+    html.document.body.style.fontSize = '100px';
+
+    final canvas = DomCanvas(domRenderer.createElement('flt-picture'));
+    Offset offset = Offset(10.0, 10.0);
+
+    final CanvasParagraph paragraph = rich(
+      ParagraphStyle(fontFamily: 'Roboto'),
+      (CanvasParagraphBuilder builder) {
+        builder.pushStyle(EngineTextStyle.only(color: yellow, fontSize: 24.0));
+        builder.addText('Lorem ');
+        builder.pushStyle(EngineTextStyle.only(color: red, fontSize: 48.0));
+        builder.addText('ipsum');
+      },
+    )..layout(constrain(double.infinity));
+    final Rect rect = Rect.fromLTWH(offset.dx, offset.dy, paragraph.maxIntrinsicWidth, paragraph.height);
+    canvas.drawRect(rect, SurfacePaintData()..color = black);
+    canvas.drawParagraph(paragraph, offset);
+    offset = offset.translate(paragraph.maxIntrinsicWidth, 0.0);
+
+    // Add some extra padding between the two paragraphs.
+    offset = offset.translate(20.0, 0.0);
+
+    // Use the same height as the previous paragraph so that the 2 paragraphs
+    // look nice in the screenshot.
+    final double placeholderHeight = paragraph.height;
+    final double placeholderWidth = paragraph.height * 2;
+
+    final CanvasParagraph paragraph2 = rich(
+      ParagraphStyle(),
+      (CanvasParagraphBuilder builder) {
+        builder.addPlaceholder(placeholderWidth, placeholderHeight, PlaceholderAlignment.baseline, baseline: TextBaseline.alphabetic);
+      },
+    )..layout(constrain(double.infinity));
+    final Rect rect2 = Rect.fromLTWH(offset.dx, offset.dy, paragraph2.maxIntrinsicWidth, paragraph2.height);
+    canvas.drawRect(rect2, SurfacePaintData()..color = black);
+    canvas.drawParagraph(paragraph2, offset);
+    // Draw a rect in the placeholder.
+    // Leave some padding around the placeholder to make the black paragraph
+    // background visible.
+    final double padding = 5;
+    final TextBox placeholderBox = paragraph2.getBoxesForPlaceholders().single;
+    canvas.drawRect(
+      placeholderBox.toRect().shift(offset).deflate(padding),
+      SurfacePaintData()..color = red,
+    );
+
+    await takeScreenshot(canvas, bounds, 'canvas_paragraph_giant_body_font_size_dom');
+
+    // Restore the old font size value.
+    html.document.body.style.fontSize = oldBodyFontSize;
+  });
+
   test('paints spans with varying heights/baselines', () {
     final canvas = BitmapCanvas(bounds, RenderStrategy());
 
diff --git a/lib/web_ui/test/text/canvas_paragraph_builder_test.dart b/lib/web_ui/test/text/canvas_paragraph_builder_test.dart
index 3cd1ff5..8c48e36 100644
--- a/lib/web_ui/test/text/canvas_paragraph_builder_test.dart
+++ b/lib/web_ui/test/text/canvas_paragraph_builder_test.dart
@@ -11,8 +11,8 @@
 bool get isIosSafari => browserEngine == BrowserEngine.webkit &&
           operatingSystem == OperatingSystem.iOs;
 
-String get defaultFontFamily {
-  String fontFamily = canonicalizeFontFamily('Ahem')!;
+String fontFamilyToAttribute(String fontFamily) {
+  fontFamily = canonicalizeFontFamily(fontFamily)!;
   if (browserEngine == BrowserEngine.firefox) {
     fontFamily = fontFamily.replaceAll('"', '&quot;');
   } else if (browserEngine == BrowserEngine.blink ||
@@ -22,6 +22,8 @@
   }
   return 'font-family: $fontFamily;';
 }
+
+final String defaultFontFamily = fontFamilyToAttribute('Ahem');
 const String defaultColor = 'color: rgb(255, 0, 0);';
 const String defaultFontSize = 'font-size: 14px;';
 final String paragraphStyle =
@@ -52,7 +54,7 @@
     paragraph.layout(ParagraphConstraints(width: double.infinity));
     expect(
       paragraph.toDomElement().outerHtml,
-      '<p style="$paragraphStyle">'
+      '<p style="font-size: 13px; $defaultFontFamily $paragraphStyle">'
       '<span style="$defaultColor font-size: 13px; $defaultFontFamily">'
       'Hello'
       '</span>'
@@ -63,7 +65,7 @@
     paragraph.layout(ParagraphConstraints(width: 39.0));
     expect(
       paragraph.toDomElement().outerHtml,
-      '<p style="$paragraphStyle">'
+      '<p style="font-size: 13px; $defaultFontFamily $paragraphStyle">'
       '<span style="$defaultColor font-size: 13px; $defaultFontFamily">'
       'Hel<br>lo'
       '</span>'
@@ -91,7 +93,11 @@
     paragraph.layout(ParagraphConstraints(width: double.infinity));
     expect(
       paragraph.toDomElement().outerHtml,
-      '<p style="$paragraphStyle"><span style="$defaultColor $defaultFontSize $defaultFontFamily">Hello</span></p>',
+      '<p style="$defaultFontSize $defaultFontFamily $paragraphStyle">'
+      '<span style="$defaultColor $defaultFontSize $defaultFontFamily">'
+      'Hello'
+      '</span>'
+      '</p>',
     );
 
     final FlatTextSpan textSpan = paragraph.spans.single as FlatTextSpan;
@@ -116,7 +122,7 @@
     paragraph.layout(ParagraphConstraints(width: double.infinity));
     expect(
       paragraph.toDomElement().outerHtml,
-      '<p style="$paragraphStyle overflow-y: hidden; height: ${expectedHeight}px;">'
+      '<p style="$defaultFontSize $defaultFontFamily $paragraphStyle overflow-y: hidden; height: ${expectedHeight}px;">'
       '<span style="$defaultColor $defaultFontSize $defaultFontFamily">'
       'Hello'
       '</span>'
@@ -142,7 +148,7 @@
     paragraph.layout(ParagraphConstraints(width: double.infinity));
     expect(
       paragraph.toDomElement().outerHtml,
-      '<p style="$paragraphStyle overflow: hidden; height: ${expectedHeight}px; text-overflow: ellipsis;">'
+      '<p style="$defaultFontSize $defaultFontFamily $paragraphStyle overflow: hidden; height: ${expectedHeight}px; text-overflow: ellipsis;">'
       '<span style="$defaultColor $defaultFontSize $defaultFontFamily">'
       'Hello'
       '</span>'
@@ -170,7 +176,7 @@
     paragraph.layout(ParagraphConstraints(width: double.infinity));
     expect(
       paragraph.toDomElement().outerHtml,
-      '<p style="line-height: 1.5; $paragraphStyle">'
+      '<p style="line-height: 1.5; font-size: 9px; $defaultFontFamily $paragraphStyle">'
       '<span style="$defaultColor line-height: 1.5; font-size: 9px; font-weight: bold; font-style: italic; $defaultFontFamily letter-spacing: 2px;">'
       'Hello'
       '</span>'
@@ -208,7 +214,7 @@
     paragraph.layout(ParagraphConstraints(width: double.infinity));
     expect(
       paragraph.toDomElement().outerHtml,
-      '<p style="$paragraphStyle">'
+      '<p style="font-size: 13px; $defaultFontFamily $paragraphStyle">'
       '<span style="$defaultColor font-size: 13px; font-weight: bold; $defaultFontFamily">'
       'Hello'
       '</span>'
@@ -222,7 +228,7 @@
     paragraph.layout(ParagraphConstraints(width: 75.0));
     expect(
       paragraph.toDomElement().outerHtml,
-      '<p style="$paragraphStyle width: 75px;">'
+      '<p style="font-size: 13px; $defaultFontFamily $paragraphStyle width: 75px;">'
       '<span style="$defaultColor font-size: 13px; font-weight: bold; $defaultFontFamily">'
       'Hello'
       '</span>'
@@ -273,7 +279,7 @@
     paragraph.layout(ParagraphConstraints(width: double.infinity));
     expect(
       paragraph.toDomElement().outerHtml,
-      '<p style="$paragraphStyle">'
+      '<p style="font-size: 13px; $defaultFontFamily $paragraphStyle">'
       '<span style="$defaultColor line-height: 2; font-size: 13px; font-weight: bold; $defaultFontFamily">'
       'Hello'
       '</span>'
@@ -337,7 +343,7 @@
     paragraph.layout(ParagraphConstraints(width: double.infinity));
     expect(
       paragraph.toDomElement().outerHtml,
-      '<p style="$paragraphStyle">'
+      '<p style="font-size: 13px; $defaultFontFamily $paragraphStyle">'
       '<span style="$defaultColor font-size: 13px; $defaultFontFamily">'
       'First<br>Second '
       '</span>'
@@ -351,7 +357,7 @@
     paragraph.layout(ParagraphConstraints(width: 180.0));
     expect(
       paragraph.toDomElement().outerHtml,
-      '<p style="$paragraphStyle width: 180px;">'
+      '<p style="font-size: 13px; $defaultFontFamily $paragraphStyle width: 180px;">'
       '<span style="$defaultColor font-size: 13px; $defaultFontFamily">'
       'First<br>Second <br>'
       '</span>'
@@ -361,6 +367,44 @@
       '</p>',
     );
   });
+
+  test('various font sizes', () {
+    // Paragraphs and spans force the Ahem font in test mode. We need to trick
+    // them into thinking they are not in test mode, so they use the provided
+    // font family.
+    debugEmulateFlutterTesterEnvironment = false;
+    final EngineParagraphStyle style = EngineParagraphStyle(fontSize: 12.0, fontFamily: 'first');
+    final CanvasParagraphBuilder builder = CanvasParagraphBuilder(style);
+
+    builder.addText('First ');
+    builder.pushStyle(TextStyle(fontSize: 18.0, fontFamily: 'second'));
+    builder.addText('Second ');
+    builder.pushStyle(TextStyle(fontSize: 10.0, fontFamily: 'third'));
+    builder.addText('Third');
+
+    final CanvasParagraph paragraph = builder.build();
+    expect(paragraph.toPlainText(), 'First Second Third');
+    expect(paragraph.spans, hasLength(3));
+
+    // The paragraph should take the font size and family from the span with the
+    // greatest font size.
+    paragraph.layout(ParagraphConstraints(width: double.infinity));
+    expect(
+      paragraph.toDomElement().outerHtml,
+      '<p style="font-size: 18px; ${fontFamilyToAttribute('second')} $paragraphStyle">'
+      '<span style="$defaultColor font-size: 12px; ${fontFamilyToAttribute('first')}">'
+      'First '
+      '</span>'
+      '<span style="$defaultColor font-size: 18px; ${fontFamilyToAttribute('second')}">'
+      'Second '
+      '</span>'
+      '<span style="$defaultColor font-size: 10px; ${fontFamilyToAttribute('third')}">'
+      'Third'
+      '</span>'
+      '</p>',
+    );
+    debugEmulateFlutterTesterEnvironment = true;
+  });
 }
 
 TextStyle styleWithDefaults({