From 25018dc78a66cf89f1cff1b641e673894821a083 Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Fri, 17 Feb 2023 14:48:49 +0100 Subject: [PATCH] feat(ui_component): add rich text builder / parser --- .../lib/src/domain/entities/entities.dart | 1 + .../entities/rich_text_builder/parser.dart | 120 ++++++++++++++++++ .../rich_text_builder/rich_text_builder.dart | 19 +++ .../rich_text_builder_component.dart | 56 ++++++++ .../rich_text_builder_component.g.dart | 25 ++++ .../rich_text_builder_style.dart | 83 ++++++++++++ 6 files changed, 304 insertions(+) create mode 100644 packages/wyatt_ui_components/lib/src/domain/entities/rich_text_builder/parser.dart create mode 100644 packages/wyatt_ui_components/lib/src/domain/entities/rich_text_builder/rich_text_builder.dart create mode 100644 packages/wyatt_ui_components/lib/src/domain/entities/rich_text_builder/rich_text_builder_component.dart create mode 100644 packages/wyatt_ui_components/lib/src/domain/entities/rich_text_builder/rich_text_builder_component.g.dart create mode 100644 packages/wyatt_ui_components/lib/src/domain/entities/rich_text_builder/rich_text_builder_style.dart diff --git a/packages/wyatt_ui_components/lib/src/domain/entities/entities.dart b/packages/wyatt_ui_components/lib/src/domain/entities/entities.dart index f04375b8..85ce40c3 100644 --- a/packages/wyatt_ui_components/lib/src/domain/entities/entities.dart +++ b/packages/wyatt_ui_components/lib/src/domain/entities/entities.dart @@ -23,4 +23,5 @@ export './error_widget_component.dart'; export './loader_component.dart'; export './loader_style.dart'; export './loading_widget_component.dart'; +export './rich_text_builder/rich_text_builder.dart'; export './theme_style.dart'; diff --git a/packages/wyatt_ui_components/lib/src/domain/entities/rich_text_builder/parser.dart b/packages/wyatt_ui_components/lib/src/domain/entities/rich_text_builder/parser.dart new file mode 100644 index 00000000..bd92d252 --- /dev/null +++ b/packages/wyatt_ui_components/lib/src/domain/entities/rich_text_builder/parser.dart @@ -0,0 +1,120 @@ +// Copyright (C) 2023 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'package:flutter/widgets.dart'; + +class RichTextStyleParameter { + const RichTextStyleParameter( + this.defaultStyle, + this.definedStyle, + this.styleName, + ); + + final TextStyle defaultStyle; + final Map definedStyle; + final String? styleName; + + TextStyle get style { + if (definedStyle.containsKey(styleName)) { + return definedStyle[styleName]!; + } + return defaultStyle; + } + + RichTextStyleParameter copyWith({ + TextStyle? defaultStyle, + Map? definedStyle, + String? styleName, + }) => + RichTextStyleParameter( + defaultStyle ?? this.defaultStyle, + definedStyle ?? this.definedStyle, + styleName ?? this.styleName, + ); +} + +class RichTextNode { + RichTextNode(this.nodes); + + final List nodes; + + static RichTextNode from( + String content, + RegExp regex, + RichTextStyleParameter styleParameter, + ) { + final matches = regex.allMatches(content); + if (matches.isNotEmpty) { + // match found -> construct node with leaf/nodes + final List nodes = []; + for (var i = 0; i < matches.length; i++) { + final previousMatch = i > 0 ? matches.elementAt(i - 1) : null; + final currentMatch = matches.elementAt(i); + // non match before + final nonMatchBefore = (previousMatch != null) + ? content.substring(previousMatch.end, currentMatch.start) + : content.substring(0, currentMatch.start); + nodes + ..add(RichTextNode.from(nonMatchBefore, regex, styleParameter)) + // match + ..add( + RichTextNode.from( + currentMatch.group(2)!, + regex, + styleParameter.copyWith(styleName: currentMatch.group(1)), + ), + ); + } + // non match after + final nonMatchAfter = content.substring(matches.last.end); + nodes.add(RichTextNode.from(nonMatchAfter, regex, styleParameter)); + return RichTextNode(nodes); + } else { + // match not found -> construct leaf + return RichTextLeaf(styleParameter.style, content); + } + } + + InlineSpan toInlineSpan(RichTextParser parser) { + final children = []; + for (final node in nodes) { + children.add(node.toInlineSpan(parser)); + } + return TextSpan(children: children); + } +} + +class RichTextLeaf extends RichTextNode { + RichTextLeaf(this.style, this.content) : super([]); + + final TextStyle style; + final String content; + + @override + InlineSpan toInlineSpan(RichTextParser parser) => + parser.nodeBuilder.call(content, style); +} + +class RichTextParser { + const RichTextParser({required this.nodeBuilder}); + factory RichTextParser.defaultBuilder() => RichTextParser( + nodeBuilder: (content, style) => TextSpan( + text: content, + style: style, + ), + ); + final InlineSpan Function(String content, TextStyle style) nodeBuilder; +} diff --git a/packages/wyatt_ui_components/lib/src/domain/entities/rich_text_builder/rich_text_builder.dart b/packages/wyatt_ui_components/lib/src/domain/entities/rich_text_builder/rich_text_builder.dart new file mode 100644 index 00000000..1eae2365 --- /dev/null +++ b/packages/wyatt_ui_components/lib/src/domain/entities/rich_text_builder/rich_text_builder.dart @@ -0,0 +1,19 @@ +// Copyright (C) 2023 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +export 'parser.dart'; +export 'rich_text_builder_component.dart'; +export 'rich_text_builder_style.dart'; diff --git a/packages/wyatt_ui_components/lib/src/domain/entities/rich_text_builder/rich_text_builder_component.dart b/packages/wyatt_ui_components/lib/src/domain/entities/rich_text_builder/rich_text_builder_component.dart new file mode 100644 index 00000000..603aaa32 --- /dev/null +++ b/packages/wyatt_ui_components/lib/src/domain/entities/rich_text_builder/rich_text_builder_component.dart @@ -0,0 +1,56 @@ +// Copyright (C) 2023 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'package:flutter/widgets.dart'; +import 'package:wyatt_component_copy_with_extension/component_copy_with_extension.dart'; +import 'package:wyatt_ui_components/wyatt_wyatt_ui_components.dart'; + +part 'rich_text_builder_component.g.dart'; + +@ComponentProxyExtension() +abstract class RichTextBuilderComponent extends Component + with CopyWithMixin<$RichTextBuilderComponentCWProxy> { + const RichTextBuilderComponent({ + this.text, + this.parser, + this.defaultStyle, + this.styles, + super.themeResolver, + super.key, + }); + + /// Full text + final String? text; + + /// How to build InlineSpans + final RichTextParser? parser; + + /// Default TextStyle used in this rich text component. + final TextStyle? defaultStyle; + + /// Used styles in this rich text component. + /// + /// e.g. + /// ```dart + /// styles = {'red': TextStyle(color: Colors.red)}; + /// ``` + /// will transform: + /// ```text + /// This text is red? styles; +} diff --git a/packages/wyatt_ui_components/lib/src/domain/entities/rich_text_builder/rich_text_builder_component.g.dart b/packages/wyatt_ui_components/lib/src/domain/entities/rich_text_builder/rich_text_builder_component.g.dart new file mode 100644 index 00000000..9c5194e6 --- /dev/null +++ b/packages/wyatt_ui_components/lib/src/domain/entities/rich_text_builder/rich_text_builder_component.g.dart @@ -0,0 +1,25 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'rich_text_builder_component.dart'; + +// ************************************************************************** +// ComponentProxyGenerator +// ************************************************************************** + +abstract class $RichTextBuilderComponentCWProxy { + RichTextBuilderComponent text(String? text); + RichTextBuilderComponent parser(RichTextParser? parser); + RichTextBuilderComponent defaultStyle(TextStyle? defaultStyle); + RichTextBuilderComponent styles(Map? styles); + RichTextBuilderComponent themeResolver( + ThemeResolver? themeResolver); + RichTextBuilderComponent key(Key? key); + RichTextBuilderComponent call({ + String? text, + RichTextParser? parser, + TextStyle? defaultStyle, + Map? styles, + ThemeResolver? themeResolver, + Key? key, + }); +} diff --git a/packages/wyatt_ui_components/lib/src/domain/entities/rich_text_builder/rich_text_builder_style.dart b/packages/wyatt_ui_components/lib/src/domain/entities/rich_text_builder/rich_text_builder_style.dart new file mode 100644 index 00000000..43a03ff5 --- /dev/null +++ b/packages/wyatt_ui_components/lib/src/domain/entities/rich_text_builder/rich_text_builder_style.dart @@ -0,0 +1,83 @@ +// Copyright (C) 2023 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'package:flutter/widgets.dart'; +import 'package:wyatt_ui_components/wyatt_wyatt_ui_components.dart'; + +class RichTextBuilderStyle extends ThemeStyle { + const RichTextBuilderStyle({ + this.defaultStyle, + this.styles, + }); + + /// Merges non-null `b` attributes in `a` + static RichTextBuilderStyle? merge( + RichTextBuilderStyle? a, + RichTextBuilderStyle? b, + ) { + if (b == null) { + return a?.copyWith(); + } + if (a == null) { + return b.copyWith(); + } + + return a.copyWith( + defaultStyle: b.defaultStyle, + styles: b.styles, + ); + } + + /// Used for interpolation. + static RichTextBuilderStyle? lerp( + RichTextBuilderStyle? a, + RichTextBuilderStyle? b, + double t, + ) { + if (a == null || b == null) { + return null; + } + // b.copyWith to return b attributes even if they are not lerped + return b.copyWith( + defaultStyle: TextStyle.lerp(a.defaultStyle, b.defaultStyle, t), + styles: b.styles, // TODO(wyatt): compute lerp value of each styles + ); + } + + /// Default TextStyle used in this rich text component. + final TextStyle? defaultStyle; + + /// Used styles in this rich text component. + final Map? styles; + + @override + RichTextBuilderStyle mergeWith(RichTextBuilderStyle? other) => + RichTextBuilderStyle.merge(this, other)!; + + @override + RichTextBuilderStyle? lerpWith(RichTextBuilderStyle? other, double t) => + RichTextBuilderStyle.lerp(this, other, t); + + @override + RichTextBuilderStyle copyWith({ + TextStyle? defaultStyle, + Map? styles, + }) => + RichTextBuilderStyle( + defaultStyle: defaultStyle ?? this.defaultStyle, + styles: styles ?? this.styles, + ); +}