diff --git a/super_editor/lib/src/core/styles.dart b/super_editor/lib/src/core/styles.dart index 9469c1c28..b01dd2ad9 100644 --- a/super_editor/lib/src/core/styles.dart +++ b/super_editor/lib/src/core/styles.dart @@ -341,4 +341,19 @@ class Styles { /// Applies a [AttributionStyleAdjuster] to a text node. static const String inlineTextStyler = 'inlineTextStyler'; + + /// Applies a [Color] to the dot of an unordered list item. + static const String dotColor = 'dotColor'; + + /// Applies a [BoxShape] to the dot of an unordered list item. + static const String dotShape = 'dotShape'; + + /// Applies a [Size] to the dot of an unordered list item. + /// + /// This is a [Size] instead of a radius because the dot can be rendered + /// as a rectangle. + static const String dotSize = 'dotSize'; + + /// Applies a [OrderedListNumeralStyle] to an ordered list item. + static const String listNumeralStyle = 'listNumeralStyle'; } diff --git a/super_editor/lib/src/default_editor/list_items.dart b/super_editor/lib/src/default_editor/list_items.dart index cba6422d7..c714349f2 100644 --- a/super_editor/lib/src/default_editor/list_items.dart +++ b/super_editor/lib/src/default_editor/list_items.dart @@ -4,6 +4,7 @@ import 'package:flutter/services.dart'; import 'package:super_editor/src/core/document_composer.dart'; import 'package:super_editor/src/core/edit_context.dart'; import 'package:super_editor/src/core/editor.dart'; +import 'package:super_editor/src/core/styles.dart'; import 'package:super_editor/src/default_editor/attributions.dart'; import 'package:super_editor/src/infrastructure/_logging.dart'; import 'package:super_editor/src/infrastructure/attributed_text_styles.dart'; @@ -130,43 +131,54 @@ class ListItemComponentBuilder implements ComponentBuilder { } } - return ListItemComponentViewModel( - nodeId: node.id, - type: node.type, - indent: node.indent, - ordinalValue: ordinalValue, - text: node.text, - textStyleBuilder: noStyleBuilder, - selectionColor: const Color(0x00000000), - ); + return switch (node.type) { + ListItemType.unordered => UnorderedListItemComponentViewModel( + nodeId: node.id, + indent: node.indent, + text: node.text, + textStyleBuilder: noStyleBuilder, + selectionColor: const Color(0x00000000), + ), + ListItemType.ordered => OrderedListItemComponentViewModel( + nodeId: node.id, + indent: node.indent, + ordinalValue: ordinalValue, + text: node.text, + textStyleBuilder: noStyleBuilder, + selectionColor: const Color(0x00000000), + ), + }; } @override Widget? createComponent( SingleColumnDocumentComponentContext componentContext, SingleColumnLayoutComponentViewModel componentViewModel) { - if (componentViewModel is! ListItemComponentViewModel) { + if (componentViewModel is! UnorderedListItemComponentViewModel && + componentViewModel is! OrderedListItemComponentViewModel) { return null; } - if (componentViewModel.type == ListItemType.unordered) { + if (componentViewModel is UnorderedListItemComponentViewModel) { return UnorderedListItemComponent( componentKey: componentContext.componentKey, text: componentViewModel.text, styleBuilder: componentViewModel.textStyleBuilder, indent: componentViewModel.indent, + dotStyle: componentViewModel.dotStyle, textSelection: componentViewModel.selection, selectionColor: componentViewModel.selectionColor, highlightWhenEmpty: componentViewModel.highlightWhenEmpty, composingRegion: componentViewModel.composingRegion, showComposingUnderline: componentViewModel.showComposingUnderline, ); - } else if (componentViewModel.type == ListItemType.ordered) { + } else if (componentViewModel is OrderedListItemComponentViewModel) { return OrderedListItemComponent( componentKey: componentContext.componentKey, indent: componentViewModel.indent, listIndex: componentViewModel.ordinalValue!, text: componentViewModel.text, styleBuilder: componentViewModel.textStyleBuilder, + numeralStyle: componentViewModel.numeralStyle, textSelection: componentViewModel.selection, selectionColor: componentViewModel.selectionColor, highlightWhenEmpty: componentViewModel.highlightWhenEmpty, @@ -181,13 +193,11 @@ class ListItemComponentBuilder implements ComponentBuilder { } } -class ListItemComponentViewModel extends SingleColumnLayoutComponentViewModel with TextComponentViewModel { +abstract class ListItemComponentViewModel extends SingleColumnLayoutComponentViewModel with TextComponentViewModel { ListItemComponentViewModel({ required String nodeId, double? maxWidth, EdgeInsetsGeometry padding = EdgeInsets.zero, - required this.type, - this.ordinalValue, required this.indent, required this.text, required this.textStyleBuilder, @@ -200,8 +210,6 @@ class ListItemComponentViewModel extends SingleColumnLayoutComponentViewModel wi this.showComposingUnderline = false, }) : super(nodeId: nodeId, maxWidth: maxWidth, padding: padding); - ListItemType type; - int? ordinalValue; int indent; @override @@ -223,25 +231,6 @@ class ListItemComponentViewModel extends SingleColumnLayoutComponentViewModel wi @override bool showComposingUnderline; - @override - ListItemComponentViewModel copy() { - return ListItemComponentViewModel( - nodeId: nodeId, - maxWidth: maxWidth, - padding: padding, - type: type, - ordinalValue: ordinalValue, - indent: indent, - text: text, - textStyleBuilder: textStyleBuilder, - textDirection: textDirection, - selection: selection, - selectionColor: selectionColor, - composingRegion: composingRegion, - showComposingUnderline: showComposingUnderline, - ); - } - @override bool operator ==(Object other) => identical(this, other) || @@ -249,8 +238,6 @@ class ListItemComponentViewModel extends SingleColumnLayoutComponentViewModel wi other is ListItemComponentViewModel && runtimeType == other.runtimeType && nodeId == other.nodeId && - type == other.type && - ordinalValue == other.ordinalValue && indent == other.indent && text == other.text && textDirection == other.textDirection && @@ -263,15 +250,156 @@ class ListItemComponentViewModel extends SingleColumnLayoutComponentViewModel wi int get hashCode => super.hashCode ^ nodeId.hashCode ^ - type.hashCode ^ - ordinalValue.hashCode ^ indent.hashCode ^ text.hashCode ^ textDirection.hashCode ^ selection.hashCode ^ selectionColor.hashCode ^ - composingRegion.hashCode ^ - showComposingUnderline.hashCode; + composingRegion.hashCode; +} + +class UnorderedListItemComponentViewModel extends ListItemComponentViewModel { + UnorderedListItemComponentViewModel({ + required super.nodeId, + super.maxWidth, + super.padding = EdgeInsets.zero, + required super.indent, + required super.text, + required super.textStyleBuilder, + this.dotStyle = const ListItemDotStyle(), + super.textDirection = TextDirection.ltr, + super.textAlignment = TextAlign.left, + super.selection, + required super.selectionColor, + super.highlightWhenEmpty = false, + super.composingRegion, + super.showComposingUnderline = false, + }); + + ListItemDotStyle dotStyle = const ListItemDotStyle(); + + @override + void applyStyles(Map styles) { + super.applyStyles(styles); + dotStyle = ListItemDotStyle( + color: styles[Styles.dotColor], + shape: styles[Styles.dotShape] ?? BoxShape.circle, + size: styles[Styles.dotSize] ?? const Size(4, 4), + ); + } + + @override + UnorderedListItemComponentViewModel copy() { + return UnorderedListItemComponentViewModel( + nodeId: nodeId, + maxWidth: maxWidth, + padding: padding, + indent: indent, + text: text, + textStyleBuilder: textStyleBuilder, + dotStyle: dotStyle, + textDirection: textDirection, + selection: selection, + selectionColor: selectionColor, + composingRegion: composingRegion, + showComposingUnderline: showComposingUnderline, + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + super == other && + other is UnorderedListItemComponentViewModel && + runtimeType == other.runtimeType && + dotStyle == other.dotStyle; + + @override + int get hashCode => super.hashCode ^ dotStyle.hashCode; +} + +class OrderedListItemComponentViewModel extends ListItemComponentViewModel { + OrderedListItemComponentViewModel({ + required super.nodeId, + super.maxWidth, + super.padding = EdgeInsets.zero, + required super.indent, + this.ordinalValue, + this.numeralStyle = OrderedListNumeralStyle.arabic, + required super.text, + required super.textStyleBuilder, + super.textDirection = TextDirection.ltr, + super.textAlignment = TextAlign.left, + super.selection, + required super.selectionColor, + super.highlightWhenEmpty = false, + super.composingRegion, + super.showComposingUnderline = false, + }); + + final int? ordinalValue; + OrderedListNumeralStyle numeralStyle; + + @override + void applyStyles(Map styles) { + super.applyStyles(styles); + numeralStyle = styles[Styles.listNumeralStyle] ?? OrderedListNumeralStyle.arabic; + } + + @override + OrderedListItemComponentViewModel copy() { + return OrderedListItemComponentViewModel( + nodeId: nodeId, + maxWidth: maxWidth, + padding: padding, + indent: indent, + ordinalValue: ordinalValue, + numeralStyle: numeralStyle, + text: text, + textStyleBuilder: textStyleBuilder, + textDirection: textDirection, + selection: selection, + selectionColor: selectionColor, + composingRegion: composingRegion, + showComposingUnderline: showComposingUnderline, + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + super == other && + other is OrderedListItemComponentViewModel && + runtimeType == other.runtimeType && + ordinalValue == other.ordinalValue && + numeralStyle == other.numeralStyle; + + @override + int get hashCode => super.hashCode ^ ordinalValue.hashCode ^ numeralStyle.hashCode; +} + +class ListItemDotStyle { + const ListItemDotStyle({ + this.color, + this.shape = BoxShape.circle, + this.size = const Size(4, 4), + }); + + final Color? color; + final BoxShape shape; + final Size size; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ListItemDotStyle && + runtimeType == other.runtimeType && + color == other.color && + shape == other.shape && + size == other.size; + + @override + int get hashCode => super.hashCode ^ color.hashCode ^ shape.hashCode ^ size.hashCode; } /// Displays a un-ordered list item in a document. @@ -284,6 +412,7 @@ class UnorderedListItemComponent extends StatefulWidget { required this.text, required this.styleBuilder, this.dotBuilder = _defaultUnorderedListItemDotBuilder, + this.dotStyle, this.indent = 0, this.indentCalculator = _defaultIndentCalculator, this.textSelection, @@ -300,6 +429,7 @@ class UnorderedListItemComponent extends StatefulWidget { final AttributedText text; final AttributionStyleBuilder styleBuilder; final UnorderedListItemDotBuilder dotBuilder; + final ListItemDotStyle? dotStyle; final int indent; final double Function(TextStyle, int indent) indentCalculator; final TextSelection? textSelection; @@ -376,6 +506,24 @@ class _UnorderedListItemComponentState extends State } } +/// The styling of an ordered list numberal. +enum OrderedListNumeralStyle { + /// Arabic numeral style (e.g. 1, 2, 3, ...). + arabic, + + /// Lowercase alphabetic numeral style (e.g. a, b, c, ...). + lowerAlpha, + + /// Uppercase alphabetic numeral style (e.g. A, B, C, ...). + upperAlpha, + + /// Lowercase Roman numeral style (e.g. i, ii, iii, ...). + lowerRoman, + + /// Uppercase Roman numeral style (e.g. I, II, III, ...). + upperRoman, +} + typedef UnorderedListItemDotBuilder = Widget Function(BuildContext, UnorderedListItemComponent); Widget _defaultUnorderedListItemDotBuilder(BuildContext context, UnorderedListItemComponent component) { @@ -385,6 +533,8 @@ Widget _defaultUnorderedListItemDotBuilder(BuildContext context, UnorderedListIt final attributions = component.text.getAllAttributionsAt(0).toSet(); final textStyle = component.styleBuilder(attributions); + final dotSize = component.dotStyle?.size ?? const Size(4, 4); + return Align( alignment: Alignment.centerRight, child: Text.rich( @@ -398,12 +548,12 @@ Widget _defaultUnorderedListItemDotBuilder(BuildContext context, UnorderedListIt WidgetSpan( alignment: PlaceholderAlignment.middle, child: Container( - width: 4, - height: 4, + width: dotSize.width, + height: dotSize.height, margin: const EdgeInsets.only(right: 10), decoration: BoxDecoration( - shape: BoxShape.circle, - color: textStyle.color, + shape: component.dotStyle?.shape ?? BoxShape.circle, + color: component.dotStyle?.color ?? textStyle.color, ), ), ), @@ -426,6 +576,7 @@ class OrderedListItemComponent extends StatefulWidget { required this.text, required this.styleBuilder, this.numeralBuilder = _defaultOrderedListItemNumeralBuilder, + this.numeralStyle = OrderedListNumeralStyle.arabic, this.indent = 0, this.indentCalculator = _defaultIndentCalculator, this.textSelection, @@ -443,6 +594,7 @@ class OrderedListItemComponent extends StatefulWidget { final AttributedText text; final AttributionStyleBuilder styleBuilder; final OrderedListItemNumeralBuilder numeralBuilder; + final OrderedListNumeralStyle numeralStyle; final int indent; final double Function(TextStyle, int indent) indentCalculator; final TextSelection? textSelection; @@ -541,7 +693,7 @@ Widget _defaultOrderedListItemNumeralBuilder(BuildContext context, OrderedListIt child: Padding( padding: const EdgeInsets.only(right: 5.0), child: Text( - '${component.listIndex}.', + '${_numeralForIndex(component.listIndex, component.numeralStyle)}.', textAlign: TextAlign.right, style: textStyle, ), @@ -550,6 +702,123 @@ Widget _defaultOrderedListItemNumeralBuilder(BuildContext context, OrderedListIt ); } +/// Returns the text to be displayed for the given [numeral] and [numeralStyle]. +String _numeralForIndex(int numeral, OrderedListNumeralStyle numeralStyle) { + return switch (numeralStyle) { + OrderedListNumeralStyle.arabic => '$numeral', + OrderedListNumeralStyle.upperRoman => _intToRoman(numeral) ?? '$numeral', + OrderedListNumeralStyle.lowerRoman => _intToRoman(numeral)?.toLowerCase() ?? '$numeral', + OrderedListNumeralStyle.upperAlpha => _intToAlpha(numeral), + OrderedListNumeralStyle.lowerAlpha => _intToAlpha(numeral).toLowerCase(), + }; +} + +/// Converts a number to its Roman numeral representation. +/// +/// Returns `null` if the number is greater than 3999, as we don't support the +/// vinculum notation. See more at https://en.wikipedia.org/wiki/Roman_numerals#cite_ref-Ifrah2000_52-1. +String? _intToRoman(int number) { + if (number <= 0) { + throw ArgumentError('Roman numerals are only defined for positive integers'); + } + + if (number > 3999) { + // Starting from 4000, the Roman numeral representation uses a bar over the numeral to represent + // a multiplication by 1000. We don't support this notation. + return null; + } + + const values = [1000, 500, 100, 50, 10, 5, 1]; + const symbols = ["M", "D", "C", "L", "X", "V", "I"]; + + int remainingValueToConvert = number; + + final result = StringBuffer(); + + for (int i = 0; i < values.length; i++) { + final currentSymbol = symbols[i]; + final currentSymbolValue = values[i]; + + final count = remainingValueToConvert ~/ currentSymbolValue; + + if (count > 0 && count < 4) { + // The number is bigger than the current symbol's value. Add the appropriate + // number of digits, respecting the maximum of three consecutive symbols. + // For example, for 300 we would add "CCC", but for 400 we won't add "CCCC". + result.write(currentSymbol * count); + + remainingValueToConvert %= currentSymbolValue; + } + + if (remainingValueToConvert <= 0) { + // The conversion is complete. + break; + } + + // We still have some value to convert. Check if we can use subtractive notation. + if (i % 2 == 0 && i + 2 < values.length) { + // Numbers in even positions (0, 2, 4) can be subtracted with other numbers + // two positions to the right of them: + // + // - 1000 (M) can be subtracted with 100 (C). + // - 100 (C) can be subtracted with 10 (X). + // - 10 (X) can be subtracted with 1 (I). + // + // Check if we can do this subtraction. + final subtractiveValue = currentSymbolValue - values[i + 2]; + if (remainingValueToConvert >= subtractiveValue) { + result.write(symbols[i + 2] + currentSymbol); + remainingValueToConvert -= subtractiveValue; + } + } else if (i % 2 != 0 && i + 1 < values.length) { + // Numbers in odd positions (1, 3, 5) can be subtracted with the number + // immediately after it to the right: + // + // - 500 (D) can be subtracted with 100 (C). + // - 50 (L) can be subtracted with 10 (X). + // - 5 (V) can be subtracted with 1 (I). + // + // Check if we can do this subtraction. + final subtractiveValue = currentSymbolValue - values[i + 1]; + if (remainingValueToConvert >= subtractiveValue) { + result.write(symbols[i + 1] + currentSymbol); + remainingValueToConvert -= subtractiveValue; + } + } + } + + return result.toString(); +} + +/// Converts a number to a string composed of A-Z characters. +/// +/// For example: +/// - 1 -> A +/// - 2 -> B +/// - ... +/// - 26 -> Z +/// - 27 -> AA +/// - 28 -> AB +String _intToAlpha(int num) { + const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + const base = characters.length; + + String result = ''; + + while (num > 0) { + // Convert to 0-based index. + num -= 1; + + // Find the next character to be added. + result = characters[num % base] + result; + + // Move to the next digit. + num = num ~/ base; + } + + return result; +} + class IndentListItemRequest implements EditRequest { IndentListItemRequest({ required this.nodeId, diff --git a/super_editor/test_goldens/editor/components/goldens/super_editor_list_item_ordered_lower_alpha_numeral.png b/super_editor/test_goldens/editor/components/goldens/super_editor_list_item_ordered_lower_alpha_numeral.png new file mode 100644 index 000000000..2f100f35d Binary files /dev/null and b/super_editor/test_goldens/editor/components/goldens/super_editor_list_item_ordered_lower_alpha_numeral.png differ diff --git a/super_editor/test_goldens/editor/components/goldens/super_editor_list_item_ordered_lower_roman_numeral.png b/super_editor/test_goldens/editor/components/goldens/super_editor_list_item_ordered_lower_roman_numeral.png new file mode 100644 index 000000000..7c71a222d Binary files /dev/null and b/super_editor/test_goldens/editor/components/goldens/super_editor_list_item_ordered_lower_roman_numeral.png differ diff --git a/super_editor/test_goldens/editor/components/goldens/super_editor_list_item_ordered_upper_alpha_numeral.png b/super_editor/test_goldens/editor/components/goldens/super_editor_list_item_ordered_upper_alpha_numeral.png new file mode 100644 index 000000000..fceaa9b31 Binary files /dev/null and b/super_editor/test_goldens/editor/components/goldens/super_editor_list_item_ordered_upper_alpha_numeral.png differ diff --git a/super_editor/test_goldens/editor/components/goldens/super_editor_list_item_ordered_upper_roman_numeral.png b/super_editor/test_goldens/editor/components/goldens/super_editor_list_item_ordered_upper_roman_numeral.png new file mode 100644 index 000000000..d4e76fc50 Binary files /dev/null and b/super_editor/test_goldens/editor/components/goldens/super_editor_list_item_ordered_upper_roman_numeral.png differ diff --git a/super_editor/test_goldens/editor/components/goldens/super_editor_list_item_unordered_custom_dot_color.png b/super_editor/test_goldens/editor/components/goldens/super_editor_list_item_unordered_custom_dot_color.png new file mode 100644 index 000000000..dd4b0587b Binary files /dev/null and b/super_editor/test_goldens/editor/components/goldens/super_editor_list_item_unordered_custom_dot_color.png differ diff --git a/super_editor/test_goldens/editor/components/goldens/super_editor_list_item_unordered_custom_dot_shape.png b/super_editor/test_goldens/editor/components/goldens/super_editor_list_item_unordered_custom_dot_shape.png new file mode 100644 index 000000000..a3cfce6b5 Binary files /dev/null and b/super_editor/test_goldens/editor/components/goldens/super_editor_list_item_unordered_custom_dot_shape.png differ diff --git a/super_editor/test_goldens/editor/components/goldens/super_editor_list_item_unordered_custom_dot_size.png b/super_editor/test_goldens/editor/components/goldens/super_editor_list_item_unordered_custom_dot_size.png new file mode 100644 index 000000000..57b1aa89f Binary files /dev/null and b/super_editor/test_goldens/editor/components/goldens/super_editor_list_item_unordered_custom_dot_size.png differ diff --git a/super_editor/test_goldens/editor/components/list_items_test.dart b/super_editor/test_goldens/editor/components/list_items_test.dart index 586db3e89..b1e0c9834 100644 --- a/super_editor/test_goldens/editor/components/list_items_test.dart +++ b/super_editor/test_goldens/editor/components/list_items_test.dart @@ -57,6 +57,69 @@ Future main() async { await screenMatchesGolden( tester, 'super_editor_list_item_unordered_aligns_dot_with_text_with_font_sizes_and_line_multiplier'); }); + + testGoldensOnMac('allows customizing the dot size', (tester) async { + await tester // + .createDocument() + .fromMarkdown('- Item 1') + .useStylesheet( + _createStylesheet().copyWith(addRulesAfter: [ + StyleRule( + const BlockSelector('listItem'), + (doc, docNode) { + return { + Styles.dotSize: const Size(14, 14), + }; + }, + ), + ]), + ) + .pump(); + + await screenMatchesGolden(tester, 'super_editor_list_item_unordered_custom_dot_size'); + }); + + testGoldensOnMac('allows customizing the dot shape', (tester) async { + await tester // + .createDocument() + .fromMarkdown('- Item 1') + .useStylesheet( + _createStylesheet().copyWith(addRulesAfter: [ + StyleRule( + const BlockSelector('listItem'), + (doc, docNode) { + return { + Styles.dotShape: BoxShape.rectangle, + }; + }, + ), + ]), + ) + .pump(); + + await screenMatchesGolden(tester, 'super_editor_list_item_unordered_custom_dot_shape'); + }); + + testGoldensOnMac('allows customizing the dot color', (tester) async { + await tester // + .createDocument() + .fromMarkdown('- Item 1') + .useStylesheet( + _createStylesheet().copyWith(addRulesAfter: [ + StyleRule( + const BlockSelector('listItem'), + (doc, docNode) { + return { + Styles.dotColor: Colors.red, + }; + }, + ), + ]), + ) + .pump(); + + await screenMatchesGolden(tester, 'super_editor_list_item_unordered_custom_dot_color'); + }); }); group('ordered', () { @@ -106,10 +169,62 @@ Future main() async { await screenMatchesGolden( tester, 'super_editor_list_item_ordered_aligns_dot_with_text_with_font_sizes_and_line_multiplier'); }); + + testGoldensOnMac('allows customizing the numeral as lower roman', (tester) async { + await _pumpOrderedListItemStyleTestApp(tester, style: OrderedListNumeralStyle.lowerRoman); + + await screenMatchesGolden(tester, 'super_editor_list_item_ordered_lower_roman_numeral'); + }); + + testGoldensOnMac('allows customizing the numeral as upper roman', (tester) async { + await _pumpOrderedListItemStyleTestApp(tester, style: OrderedListNumeralStyle.upperRoman); + + await screenMatchesGolden(tester, 'super_editor_list_item_ordered_upper_roman_numeral'); + }); + + testGoldensOnMac('allows customizing the numeral as lower alpha', (tester) async { + await _pumpOrderedListItemStyleTestApp(tester, style: OrderedListNumeralStyle.lowerAlpha); + + await screenMatchesGolden(tester, 'super_editor_list_item_ordered_lower_alpha_numeral'); + }); + + testGoldensOnMac('allows customizing the numeral as upper alpha', (tester) async { + await _pumpOrderedListItemStyleTestApp(tester, style: OrderedListNumeralStyle.upperAlpha); + + await screenMatchesGolden(tester, 'super_editor_list_item_ordered_upper_alpha_numeral'); + }); }); }); } +Future _pumpOrderedListItemStyleTestApp( + WidgetTester tester, { + required OrderedListNumeralStyle style, +}) async { + await tester // + .createDocument() + .withCustomContent(MutableDocument(nodes: [ + for (int i = 1; i <= 10; i++) + ListItemNode.ordered( + id: Editor.createNodeId(), + text: AttributedText('Item $i.'), + ) + ])) + .useStylesheet( + _createStylesheet().copyWith(addRulesAfter: [ + StyleRule( + const BlockSelector('listItem'), + (doc, docNode) { + return { + Styles.listNumeralStyle: style, + }; + }, + ), + ]), + ) + .pump(); +} + ListItemNode _createListItemNode({ required String text, required double fontSize,