diff --git a/.cspell/misc.txt b/.cspell/misc.txt index 17fe8168538..d9f301f3713 100644 --- a/.cspell/misc.txt +++ b/.cspell/misc.txt @@ -9,6 +9,7 @@ deeplinking flathub foss fullscreen +noto persistences pkgs playstore @@ -17,8 +18,12 @@ provokateurin punycode rescan STRFTIME +strikethrough subroutes +tbody testexample +thead +tilda uncategorized upsert viewbox diff --git a/packages/neon_framework/packages/neon_rich_text/lib/src/rich_text.dart b/packages/neon_framework/packages/neon_rich_text/lib/src/rich_text.dart index a939e9759ae..0877a619be7 100644 --- a/packages/neon_framework/packages/neon_rich_text/lib/src/rich_text.dart +++ b/packages/neon_framework/packages/neon_rich_text/lib/src/rich_text.dart @@ -1,24 +1,81 @@ import 'package:built_collection/built_collection.dart'; import 'package:built_value/json_object.dart'; import 'package:flutter/gestures.dart'; -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; import 'package:intersperse/intersperse.dart'; +import 'package:markdown/markdown.dart' as md; +import 'package:neon_framework/models.dart'; +import 'package:neon_framework/widgets.dart'; import 'package:neon_rich_text/src/rich_objects/rich_objects.dart'; import 'package:nextcloud/core.dart' as core; /// Renders the [text] as a rich [TextSpan]. TextSpan buildRichTextSpan({ + required Account account, required String text, + required TextStyle textStyle, required BuiltMap> parameters, required BuiltList references, - required TextStyle style, required void Function(String reference) onReferenceClicked, + required bool isMarkdown, bool isPreview = false, }) { + assert( + !isPreview || !isMarkdown, + 'A preview must not be markdown', + ); + if (isPreview) { text = text.replaceAll('\n', ' '); } + if (!isMarkdown) { + return _buildRichObjectSpan( + text: text, + textStyle: textStyle, + parameters: parameters, + isPreview: isPreview, + references: references, + onReferenceClicked: onReferenceClicked, + ); + } + + final document = md.Document( + extensionSet: md.ExtensionSet.gitHubFlavored, + encodeHtml: false, + ); + + final nodes = document.parse(text); + + final spans = _buildMarkdownSpans( + account: account, + nodes: nodes, + textStyle: textStyle, + parameters: parameters, + onReferenceClicked: onReferenceClicked, + isPreview: isPreview, + ); + + // Here we can just ignore the final newline + return TextSpan( + children: spans.children, + style: textStyle, + ); +} + +TextSpan _buildRichObjectSpan({ + required String text, + required TextStyle textStyle, + required BuiltMap> parameters, + required bool isPreview, + BuiltList? references, + void Function(String reference)? onReferenceClicked, +}) { + assert( + (references == null) == (onReferenceClicked == null), + 'Pass both references and onReferenceClicked or neither of them.', + ); + final unusedParameters = {}; var parts = [text]; @@ -42,15 +99,17 @@ TextSpan buildRichTextSpan({ parts = newParts; } - for (final reference in references) { - final newParts = []; + if (references != null) { + for (final reference in references) { + final newParts = []; - for (final part in parts) { - final p = part.split(reference); - newParts.addAll(p.intersperse(reference)); - } + for (final part in parts) { + final p = part.split(reference); + newParts.addAll(p.intersperse(reference)); + } - parts = newParts; + parts = newParts; + } } final children = []; @@ -61,7 +120,7 @@ TextSpan buildRichTextSpan({ ..add( buildRichObjectParameter( parameter: entry.value, - textStyle: style, + textStyle: textStyle, isPreview: isPreview, ), ) @@ -82,7 +141,7 @@ TextSpan buildRichTextSpan({ parameter: core.RichObjectParameter.fromJson( entry.value.map((key, value) => MapEntry(key, value.toString())).toMap(), ), - textStyle: style, + textStyle: textStyle, isPreview: isPreview, ), ); @@ -90,29 +149,28 @@ TextSpan buildRichTextSpan({ break; } } - for (final reference in references) { - if (reference == part) { - final gestureRecognizer = TapGestureRecognizer()..onTap = () => onReferenceClicked(reference); + if (references != null) { + for (final reference in references) { + if (reference == part) { + final gestureRecognizer = TapGestureRecognizer()..onTap = () => onReferenceClicked!(reference); - children.add( - TextSpan( - text: part, - style: style.copyWith( - decoration: TextDecoration.underline, - decorationThickness: 2, + children.add( + TextSpan( + text: part, + style: textStyle.merge(_linkTextStyle), + recognizer: gestureRecognizer, ), - recognizer: gestureRecognizer, - ), - ); - match = true; - break; + ); + match = true; + break; + } } } if (!match) { children.add( TextSpan( - style: style, + style: textStyle, text: part, ), ); @@ -120,7 +178,459 @@ TextSpan buildRichTextSpan({ } return TextSpan( - style: style, + style: textStyle, + children: children, + ); +} + +const _linkTextStyle = TextStyle( + decoration: TextDecoration.underline, + decorationThickness: 2, +); + +enum _MarkdownListType { + unordered, + ordered, +} + +const _markdownNewlineTags = [ + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'li', + 'p', +]; + +({List children, bool finalNewline}) _buildMarkdownSpans({ + required Account account, + required List nodes, + required TextStyle textStyle, + required BuiltMap> parameters, + required void Function(String reference) onReferenceClicked, + required bool isPreview, + _MarkdownListType? listType, + int listDepth = 0, +}) { + final children = []; + var finalNewline = false; + + for (var i = 0; i < nodes.length; i++) { + final node = nodes[i]; + final span = _buildMarkdownSpan( + account: account, + node: node, + textStyle: textStyle, + parameters: parameters, + onReferenceClicked: onReferenceClicked, + isPreview: isPreview, + listType: listType, + listIndex: i, + listDepth: listDepth, + ); + + children.add(span.child); + + if (span.finalNewline || node is md.Element && _markdownNewlineTags.contains(node.tag)) { + if (i == nodes.length - 1) { + finalNewline = true; + } else { + children.add(const TextSpan(text: '\n')); + } + } + } + + return ( children: children, + finalNewline: finalNewline, ); } + +({InlineSpan child, bool finalNewline}) _buildMarkdownSpan({ + required Account account, + required md.Node node, + required TextStyle textStyle, + required BuiltMap> parameters, + required void Function(String reference) onReferenceClicked, + required bool isPreview, + _MarkdownListType? listType, + int listIndex = 0, + int listDepth = 0, +}) { + if (node is md.Text) { + var text = node.text; + final finalNewline = text.endsWith('\n'); + if (finalNewline) { + text = text.substring(0, text.length - 1); + } + + return ( + child: _buildRichObjectSpan( + text: text, + textStyle: textStyle, + parameters: parameters, + isPreview: isPreview, + // Don't pass references and onReferenceClicked, as we already resolve them separately. + ), + finalNewline: finalNewline, + ); + } + + if (node is md.Element) { + var localTextStyle = textStyle; + var localListType = listType; + var localListDepth = listDepth; + + final childNodes = node.children ?? []; + + switch (node.tag) { + case 'a': + // This introduces a difference between the links resolved by the Reference API and the Markdown parser. + // The web frontend has the exact same issue, where Markdown embedded links are turned into links inline, but + // there is no preview displayed below the message because the Regex does not match the Markdown link. + // A bug report was filed upstream and if it is fixed in the Reference API we will automatically benefit from it as well: + // https://github.com/nextcloud/spreed/issues/13756 + final href = node.attributes['href']; + if (href != null) { + final gestureRecognizer = TapGestureRecognizer()..onTap = () => onReferenceClicked(href); + + return ( + child: TextSpan( + text: (childNodes[0] as md.Text).text, + style: localTextStyle.merge(_linkTextStyle), + recognizer: gestureRecognizer, + ), + finalNewline: false, + ); + } + case 'blockquote': + final spans = _buildMarkdownSpans( + account: account, + nodes: childNodes, + textStyle: localTextStyle, + parameters: parameters, + onReferenceClicked: onReferenceClicked, + isPreview: isPreview, + listType: localListType, + listDepth: localListDepth, + ); + + return ( + child: WidgetSpan( + child: Container( + margin: const EdgeInsets.only(left: 5), + padding: const EdgeInsets.only(left: 5), + decoration: const BoxDecoration( + border: Border( + left: BorderSide( + color: Colors.grey, + width: 2.5, + ), + ), + ), + child: Text.rich( + TextSpan( + children: spans.children, + style: localTextStyle.copyWith( + color: Colors.grey, + ), + ), + ), + ), + ), + finalNewline: spans.finalNewline, + ); + case 'code': + const backgroundColor = Colors.black; + const foregroundColor = Colors.white; + + localTextStyle = localTextStyle.copyWith( + fontFamily: 'monospace', + backgroundColor: backgroundColor, + color: foregroundColor, + ); + + final spans = _buildMarkdownSpans( + account: account, + nodes: childNodes, + textStyle: localTextStyle, + parameters: parameters, + onReferenceClicked: onReferenceClicked, + isPreview: isPreview, + listType: localListType, + listDepth: localListDepth, + ); + + // Inline code + if (!spans.finalNewline) { + return ( + child: TextSpan( + children: spans.children, + style: localTextStyle, + ), + finalNewline: spans.finalNewline, + ); + } + + return ( + child: WidgetSpan( + child: Container( + color: backgroundColor, + padding: const EdgeInsets.all(8), + child: Text.rich( + TextSpan( + children: spans.children, + style: localTextStyle, + ), + ), + ), + ), + finalNewline: spans.finalNewline, + ); + case 'del': + localTextStyle = localTextStyle.copyWith( + decoration: TextDecoration.lineThrough, + ); + case 'em': + localTextStyle = localTextStyle.copyWith( + fontStyle: FontStyle.italic, + ); + case 'hr': + return ( + child: const WidgetSpan( + child: Divider(), + ), + finalNewline: false, + ); + case 'h1': + localTextStyle = localTextStyle.copyWith( + fontSize: 32, + fontWeight: FontWeight.bold, + ); + case 'h2': + localTextStyle = localTextStyle.copyWith( + fontSize: 24, + fontWeight: FontWeight.bold, + ); + case 'h3': + localTextStyle = localTextStyle.copyWith( + fontSize: 18.72, + fontWeight: FontWeight.bold, + ); + case 'h4': + localTextStyle = localTextStyle.copyWith( + fontSize: 16, + fontWeight: FontWeight.bold, + ); + case 'h5': + localTextStyle = localTextStyle.copyWith( + fontSize: 13.28, + fontWeight: FontWeight.bold, + ); + case 'h6': + localTextStyle = localTextStyle.copyWith( + fontSize: 10.72, + fontWeight: FontWeight.bold, + ); + case 'img': + return ( + child: WidgetSpan( + child: Tooltip( + message: node.attributes['alt'], + child: NeonUriImage( + uri: Uri.parse(node.attributes['src']!), + account: account, + ), + ), + ), + finalNewline: false, + ); + case 'input': + final type = node.attributes['type']!; + switch (type) { + case 'checkbox': + final checked = node.attributes['checked'] == 'true'; + + return ( + child: WidgetSpan( + child: Checkbox( + value: checked, + onChanged: null, + visualDensity: const VisualDensity( + horizontal: VisualDensity.minimumDensity, + vertical: VisualDensity.minimumDensity, + ), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + ), + finalNewline: false, + ); + default: + throw UnimplementedError('Unexpected input type $type.'); // coverage:ignore-line + } + case 'li': + localListDepth++; + + final spans = _buildMarkdownSpans( + account: account, + nodes: childNodes, + textStyle: localTextStyle, + parameters: parameters, + onReferenceClicked: onReferenceClicked, + isPreview: isPreview, + listType: localListType, + listDepth: localListDepth, + ); + + int? subListIndex; + for (var i = 0; i < childNodes.length; i++) { + final childNode = childNodes[i]; + if (childNode is md.Element && (childNode.tag == 'ol' || childNode.tag == 'ul')) { + subListIndex = i; + break; + } + } + + InlineSpan prefix; + final firstChildNode = childNodes.firstOrNull; + if (firstChildNode case md.Element(tag: 'input')) { + prefix = const TextSpan(); + } else { + prefix = switch (localListType) { + _MarkdownListType.ordered => TextSpan(text: '${listIndex + 1}. '), + _MarkdownListType.unordered => const TextSpan(text: 'ยท '), + _ => throw ArgumentError('List type must be specified when visiting li element.'), // coverage:ignore-line + }; + } + + return ( + child: TextSpan( + children: [ + TextSpan( + text: ''.padRight(localListDepth), + style: const TextStyle( + fontFamily: 'monospace', + ), + ), + prefix, + ...spans.children.sublist(0, subListIndex), + if (subListIndex != null) const TextSpan(text: '\n'), + if (subListIndex != null) ...spans.children.sublist(subListIndex), + ], + ), + finalNewline: spans.finalNewline, + ); + case 'ol': + localListType = _MarkdownListType.ordered; + case 'p': + // Do nothing, a final newline will be inserted by the parent _buildMarkdownSpans() call. + break; + case 'pre': + localTextStyle = localTextStyle.copyWith( + fontFamily: 'monospace', + ); + case 'section': + // Do nothing, no special rendering. + case 'strong': + localTextStyle = localTextStyle.copyWith( + fontWeight: FontWeight.bold, + ); + case 'sup': + localTextStyle = localTextStyle.copyWith( + fontFeatures: [ + const FontFeature.superscripts(), + ], + ); + case 'table': + final head = childNodes.firstWhere( + (childNode) => childNode is md.Element && childNode.tag == 'thead', + ) as md.Element; + final columns = (head.children!.first as md.Element).children!.map((childNode) => childNode as md.Element); + + final body = childNodes.firstWhere( + (childNode) => childNode is md.Element && childNode.tag == 'tbody', + ) as md.Element; + final rows = body.children!.map( + (childNode) => (childNode as md.Element).children!.map( + (childNode) => childNode as md.Element, + ), + ); + + return ( + child: WidgetSpan( + child: DataTable( + columns: [ + for (final column in columns) + DataColumn( + headingRowAlignment: + column.attributes['align'] == 'right' ? MainAxisAlignment.end : MainAxisAlignment.start, + label: Text.rich( + _buildMarkdownSpan( + account: account, + node: column.children!.first, + textStyle: textStyle, + parameters: parameters, + onReferenceClicked: onReferenceClicked, + isPreview: isPreview, + ).child, + ), + ), + ], + rows: [ + for (final row in rows) + DataRow( + cells: [ + for (final cell in row) + DataCell( + Align( + alignment: + cell.attributes['align'] == 'right' ? Alignment.centerRight : Alignment.centerLeft, + child: Text.rich( + _buildMarkdownSpan( + account: account, + node: cell.children!.first, + textStyle: textStyle, + parameters: parameters, + onReferenceClicked: onReferenceClicked, + isPreview: isPreview, + ).child, + ), + ), + ), + ], + ), + ], + ), + ), + finalNewline: true, + ); + case 'ul': + localListType = _MarkdownListType.unordered; + default: + throw UnimplementedError('Unexpected element tag ${node.tag}.'); // coverage:ignore-line + } + + final spans = _buildMarkdownSpans( + account: account, + nodes: childNodes, + textStyle: localTextStyle, + parameters: parameters, + onReferenceClicked: onReferenceClicked, + isPreview: isPreview, + listType: localListType, + listDepth: localListDepth, + ); + + return ( + child: TextSpan( + children: spans.children, + style: localTextStyle, + ), + finalNewline: spans.finalNewline, + ); + } + + throw UnimplementedError('Unexpected node type ${node.runtimeType}.'); // coverage:ignore-line +} diff --git a/packages/neon_framework/packages/neon_rich_text/pubspec.yaml b/packages/neon_framework/packages/neon_rich_text/pubspec.yaml index 1b29d51e1de..d8e061a6832 100644 --- a/packages/neon_framework/packages/neon_rich_text/pubspec.yaml +++ b/packages/neon_framework/packages/neon_rich_text/pubspec.yaml @@ -14,6 +14,7 @@ dependencies: sdk: flutter flutter_material_design_icons: ^1.1.7296 intersperse: ^2.0.0 + markdown: ^7.0.0 meta: ^1.0.0 neon_framework: git: diff --git a/packages/neon_framework/packages/neon_rich_text/test/fonts/LICENSE_OFL.txt b/packages/neon_framework/packages/neon_rich_text/test/fonts/LICENSE_OFL.txt new file mode 100644 index 00000000000..09f020bb988 --- /dev/null +++ b/packages/neon_framework/packages/neon_rich_text/test/fonts/LICENSE_OFL.txt @@ -0,0 +1,93 @@ +Copyright 2022 The Noto Project Authors (https://github.com/notofonts/latin-greek-cyrillic) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/packages/neon_framework/packages/neon_rich_text/test/fonts/NotoSans-Regular.ttf b/packages/neon_framework/packages/neon_rich_text/test/fonts/NotoSans-Regular.ttf new file mode 100644 index 00000000000..4bac02f2f46 Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/fonts/NotoSans-Regular.ttf differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/fonts/NotoSansMono-Regular.ttf b/packages/neon_framework/packages/neon_rich_text/test/fonts/NotoSansMono-Regular.ttf new file mode 100644 index 00000000000..c2bbb5a3943 Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/fonts/NotoSansMono-Regular.ttf differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_XML-like_text_(escaped_and_unescaped).png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_XML-like_text_(escaped_and_unescaped).png new file mode 100644 index 00000000000..1d957b40ce6 Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_XML-like_text_(escaped_and_unescaped).png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_blockquote_(divided_from_normal_text).png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_blockquote_(divided_from_normal_text).png new file mode 100644 index 00000000000..c588bd141be Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_blockquote_(divided_from_normal_text).png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_blockquote_(with_bold,_italic_text,_inline_code).png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_blockquote_(with_bold,_italic_text,_inline_code).png new file mode 100644 index 00000000000..8b989730936 Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_blockquote_(with_bold,_italic_text,_inline_code).png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_blockquote_(with_greater_then_(>)_syntax_-_escaped).png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_blockquote_(with_greater_then_(>)_syntax_-_escaped).png new file mode 100644 index 00000000000..80f4a801ba6 Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_blockquote_(with_greater_then_(>)_syntax_-_escaped).png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_blockquote_(with_greater_then_(>)_syntax_-_normal).png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_blockquote_(with_greater_then_(>)_syntax_-_normal).png new file mode 100644 index 00000000000..4071d292c26 Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_blockquote_(with_greater_then_(>)_syntax_-_normal).png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_blockquote_(with_nested_blockquote).png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_blockquote_(with_nested_blockquote).png new file mode 100644 index 00000000000..ea7f98c538a Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_blockquote_(with_nested_blockquote).png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_blockquote_(with_several_lines).png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_blockquote_(with_several_lines).png new file mode 100644 index 00000000000..9aee4c8fcbd Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_blockquote_(with_several_lines).png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_blockquote_(with_several_paragraphs).png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_blockquote_(with_several_paragraphs).png new file mode 100644 index 00000000000..d79f178755c Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_blockquote_(with_several_paragraphs).png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_bold_text_(between_normal_texts_with_asterisk_syntax).png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_bold_text_(between_normal_texts_with_asterisk_syntax).png new file mode 100644 index 00000000000..8ec46f8dbd3 Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_bold_text_(between_normal_texts_with_asterisk_syntax).png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_bold_text_(several_in_line_with_different_syntax).png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_bold_text_(several_in_line_with_different_syntax).png new file mode 100644 index 00000000000..bd0847ce21a Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_bold_text_(several_in_line_with_different_syntax).png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_bold_text_(single_with_asterisk_syntax).png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_bold_text_(single_with_asterisk_syntax).png new file mode 100644 index 00000000000..3964e8037cb Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_bold_text_(single_with_asterisk_syntax).png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_bold_text_(single_with_underscore_syntax).png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_bold_text_(single_with_underscore_syntax).png new file mode 100644 index 00000000000..12cd545b51e Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_bold_text_(single_with_underscore_syntax).png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_dividers_(with_different_syntax).png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_dividers_(with_different_syntax).png new file mode 100644 index 00000000000..cfd137d066d Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_dividers_(with_different_syntax).png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_empty_multiline_code.png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_empty_multiline_code.png new file mode 100644 index 00000000000..8380028d671 Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_empty_multiline_code.png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_empty_multiline_code_(with_new_line).png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_empty_multiline_code_(with_new_line).png new file mode 100644 index 00000000000..8380028d671 Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_empty_multiline_code_(with_new_line).png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_heading_(with_hash_(#)_syntax_divided_with_space_from_text).png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_heading_(with_hash_(#)_syntax_divided_with_space_from_text).png new file mode 100644 index 00000000000..6a1b9970454 Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_heading_(with_hash_(#)_syntax_divided_with_space_from_text).png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_heading_1_(with_equal_(=)_syntax_on_the_next_line).png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_heading_1_(with_equal_(=)_syntax_on_the_next_line).png new file mode 100644 index 00000000000..3ad4f9ffe25 Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_heading_1_(with_equal_(=)_syntax_on_the_next_line).png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_heading_2_(with_dash_(-)_syntax_on_the_next_line).png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_heading_2_(with_dash_(-)_syntax_on_the_next_line).png new file mode 100644 index 00000000000..f55e82adf0c Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_heading_2_(with_dash_(-)_syntax_on_the_next_line).png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_ignored_bold_text_(between_normal_texts_with_underscore_syntax).png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_ignored_bold_text_(between_normal_texts_with_underscore_syntax).png new file mode 100644 index 00000000000..4fc0540d0d3 Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_ignored_bold_text_(between_normal_texts_with_underscore_syntax).png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_ignored_heading_(with_hash_(#)_syntax_padded_to_the_text).png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_ignored_heading_(with_hash_(#)_syntax_padded_to_the_text).png new file mode 100644 index 00000000000..870cd78f97e Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_ignored_heading_(with_hash_(#)_syntax_padded_to_the_text).png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_ignored_italic_text_(between_normal_texts_with_underscore_syntax).png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_ignored_italic_text_(between_normal_texts_with_underscore_syntax).png new file mode 100644 index 00000000000..c3a61f3665c Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_ignored_italic_text_(between_normal_texts_with_underscore_syntax).png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_image.png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_image.png new file mode 100644 index 00000000000..31ae5cd0ee4 Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_image.png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_inline_code_(between_normal_texts).png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_inline_code_(between_normal_texts).png new file mode 100644 index 00000000000..bfe67fd8c6c Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_inline_code_(between_normal_texts).png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_inline_code_(several_in_line).png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_inline_code_(several_in_line).png new file mode 100644 index 00000000000..bb849f6a639 Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_inline_code_(several_in_line).png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_inline_code_(single_with_backticks_syntax).png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_inline_code_(single_with_backticks_syntax).png new file mode 100644 index 00000000000..cf2ba9a4762 Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_inline_code_(single_with_backticks_syntax).png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_inline_code_(single_with_double_backticks_syntax).png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_inline_code_(single_with_double_backticks_syntax).png new file mode 100644 index 00000000000..cf2ba9a4762 Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_inline_code_(single_with_double_backticks_syntax).png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_inline_code_(single_with_triple_backticks_syntax).png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_inline_code_(single_with_triple_backticks_syntax).png new file mode 100644 index 00000000000..cf2ba9a4762 Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_inline_code_(single_with_triple_backticks_syntax).png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_inline_code_(with_ignored_bold,_italic,_XML-like_syntax).png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_inline_code_(with_ignored_bold,_italic,_XML-like_syntax).png new file mode 100644 index 00000000000..704d173a77a Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_inline_code_(with_ignored_bold,_italic,_XML-like_syntax).png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_italic_text_(between_normal_texts_with_asterisk_syntax).png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_italic_text_(between_normal_texts_with_asterisk_syntax).png new file mode 100644 index 00000000000..7ee03d379a7 Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_italic_text_(between_normal_texts_with_asterisk_syntax).png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_italic_text_(several_in_line_with_different_syntax).png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_italic_text_(several_in_line_with_different_syntax).png new file mode 100644 index 00000000000..d748bc508ad Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_italic_text_(several_in_line_with_different_syntax).png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_italic_text_(single_with_asterisk_syntax).png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_italic_text_(single_with_asterisk_syntax).png new file mode 100644 index 00000000000..57369ebeaaa Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_italic_text_(single_with_asterisk_syntax).png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_italic_text_(single_with_underscore_syntax).png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_italic_text_(single_with_underscore_syntax).png new file mode 100644 index 00000000000..87ff678b047 Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_italic_text_(single_with_underscore_syntax).png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_multiline_code_(ignored_info).png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_multiline_code_(ignored_info).png new file mode 100644 index 00000000000..a972288c04f Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_multiline_code_(ignored_info).png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_multiline_code_(with_ignored_bold,_italic,_inline_code,_XML-like_syntax).png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_multiline_code_(with_ignored_bold,_italic,_inline_code,_XML-like_syntax).png new file mode 100644 index 00000000000..0c63c8f1360 Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_multiline_code_(with_ignored_bold,_italic,_inline_code,_XML-like_syntax).png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_multiline_code_(with_several_lines).png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_multiline_code_(with_several_lines).png new file mode 100644 index 00000000000..d61186fbfa7 Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_multiline_code_(with_several_lines).png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_multiline_code_(with_triple_backticks_syntax).png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_multiline_code_(with_triple_backticks_syntax).png new file mode 100644 index 00000000000..a972288c04f Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_multiline_code_(with_triple_backticks_syntax).png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_nested_lists.png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_nested_lists.png new file mode 100644 index 00000000000..cd69c4a1194 Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_nested_lists.png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_normal_text_(between_bold_texts_with_asterisk_syntax).png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_normal_text_(between_bold_texts_with_asterisk_syntax).png new file mode 100644 index 00000000000..1a903d6e608 Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_normal_text_(between_bold_texts_with_asterisk_syntax).png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_normal_text_(between_italic_texts_with_asterisk_syntax).png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_normal_text_(between_italic_texts_with_asterisk_syntax).png new file mode 100644 index 00000000000..87d8cebbc04 Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_normal_text_(between_italic_texts_with_asterisk_syntax).png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_ordered_list_(with_number_+_`.`_syntax_divided_with_space_from_text).png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_ordered_list_(with_number_+_`.`_syntax_divided_with_space_from_text).png new file mode 100644 index 00000000000..ab1ee099587 Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_ordered_list_(with_number_+_`.`_syntax_divided_with_space_from_text).png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_reference_embedded.png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_reference_embedded.png new file mode 100644 index 00000000000..82b826cb8c7 Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_reference_embedded.png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_reference_plain.png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_reference_plain.png new file mode 100644 index 00000000000..93b8ad08e02 Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_reference_plain.png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_rich_object.png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_rich_object.png new file mode 100644 index 00000000000..87fe75a525e Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_rich_object.png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_strikethrough_text_(between_normal_texts_with_different_syntax).png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_strikethrough_text_(between_normal_texts_with_different_syntax).png new file mode 100644 index 00000000000..43e138e259e Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_strikethrough_text_(between_normal_texts_with_different_syntax).png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_strikethrough_text_(several_in_line_with_different_syntax).png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_strikethrough_text_(several_in_line_with_different_syntax).png new file mode 100644 index 00000000000..85028ce624a Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_strikethrough_text_(several_in_line_with_different_syntax).png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_strikethrough_text_(with_double_tilda_syntax).png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_strikethrough_text_(with_double_tilda_syntax).png new file mode 100644 index 00000000000..835b0bd4913 Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_strikethrough_text_(with_double_tilda_syntax).png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_strikethrough_text_(with_single_tilda_syntax).png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_strikethrough_text_(with_single_tilda_syntax).png new file mode 100644 index 00000000000..25121022fd2 Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_strikethrough_text_(with_single_tilda_syntax).png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_subscript.png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_subscript.png new file mode 100644 index 00000000000..a15e347ddd3 Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_subscript.png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_table_(with_`--_|_--`_syntax).png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_table_(with_`--_|_--`_syntax).png new file mode 100644 index 00000000000..7f9b12a271e Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_table_(with_`--_|_--`_syntax).png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_task_list_(with_`-_[_]`_and_`-_[x]`_syntax_divided_with_space_from_text).png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_task_list_(with_`-_[_]`_and_`-_[x]`_syntax_divided_with_space_from_text).png new file mode 100644 index 00000000000..e3d6373f576 Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_task_list_(with_`-_[_]`_and_`-_[x]`_syntax_divided_with_space_from_text).png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_unordered_list_(with_unite_syntax_divided_with_space_from_text).png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_unordered_list_(with_unite_syntax_divided_with_space_from_text).png new file mode 100644 index 00000000000..57fccf01e3a Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_unordered_list_(with_unite_syntax_divided_with_space_from_text).png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_unordered_lists_(with_different_syntax_divided_with_space_from_text).png b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_unordered_lists_(with_different_syntax_divided_with_space_from_text).png new file mode 100644 index 00000000000..57fccf01e3a Binary files /dev/null and b/packages/neon_framework/packages/neon_rich_text/test/goldens/rich_text_unordered_lists_(with_different_syntax_divided_with_space_from_text).png differ diff --git a/packages/neon_framework/packages/neon_rich_text/test/rich_text_test.dart b/packages/neon_framework/packages/neon_rich_text/test/rich_text_test.dart index 38bb8d85eeb..489ef1bb401 100644 --- a/packages/neon_framework/packages/neon_rich_text/test/rich_text_test.dart +++ b/packages/neon_framework/packages/neon_rich_text/test/rich_text_test.dart @@ -1,9 +1,13 @@ +import 'dart:io'; + import 'package:built_collection/built_collection.dart'; import 'package:built_value/json_object.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:neon_framework/testing.dart'; import 'package:neon_rich_text/neon_rich_text.dart'; import 'package:nextcloud/core.dart' as core; @@ -15,19 +19,23 @@ void main() { group('buildRichTextSpan', () { test('Preview without newlines', () { var span = buildRichTextSpan( + account: MockAccount(), text: '123\n456', + isMarkdown: false, parameters: BuiltMap(), references: BuiltList(), - style: const TextStyle(), + textStyle: const TextStyle(), onReferenceClicked: (_) {}, ).children!.single as TextSpan; expect(span.text, '123\n456'); span = buildRichTextSpan( + account: MockAccount(), text: '123\n456', + isMarkdown: false, parameters: BuiltMap(), references: BuiltList(), - style: const TextStyle(), + textStyle: const TextStyle(), onReferenceClicked: (_) {}, isPreview: true, ).children!.single as TextSpan; @@ -38,7 +46,9 @@ void main() { for (final type in core.RichObjectParameter_Type.values) { test(type, () { final spans = buildRichTextSpan( + account: MockAccount(), text: 'test', + isMarkdown: false, parameters: BuiltMap({ type.value: BuiltMap({ 'type': JsonObject(type.value), @@ -47,7 +57,7 @@ void main() { }), }), references: BuiltList(), - style: const TextStyle(), + textStyle: const TextStyle(), onReferenceClicked: (_) {}, ).children!; if (type == core.RichObjectParameter_Type.file) { @@ -64,7 +74,9 @@ void main() { test('Used parameters', () { final spans = buildRichTextSpan( + account: MockAccount(), text: '123 {actor1} 456 {actor2} 789', + isMarkdown: false, parameters: BuiltMap({ 'actor1': BuiltMap({ 'type': JsonObject('user'), @@ -78,7 +90,7 @@ void main() { }), }), references: BuiltList(), - style: const TextStyle(), + textStyle: const TextStyle(), onReferenceClicked: (_) {}, ).children!; expect(spans, hasLength(5)); @@ -93,10 +105,12 @@ void main() { final callback = MockOnReferenceClickedCallback(); final spans = buildRichTextSpan( + account: MockAccount(), text: 'a 123 b 456 c', + isMarkdown: false, parameters: BuiltMap(), references: BuiltList(['123', '456']), - style: const TextStyle(), + textStyle: const TextStyle(), onReferenceClicked: callback.call, ).children!; expect(spans, hasLength(5)); @@ -120,7 +134,9 @@ void main() { test('Skip empty parts', () { final spans = buildRichTextSpan( + account: MockAccount(), text: '{actor}', + isMarkdown: false, parameters: BuiltMap({ 'actor': BuiltMap({ 'type': JsonObject(core.RichObjectParameter_Type.user.name), @@ -129,11 +145,421 @@ void main() { }), }), references: BuiltList(), - style: const TextStyle(), + textStyle: const TextStyle(), onReferenceClicked: (_) {}, ).children!; expect(spans, hasLength(1)); expect((spans[0] as WidgetSpan).child, isA()); }); + + group('Markdown', () { + const defaultFontFamily = 'NotoSans-Regular'; + + setUpAll(() async { + FakeNeonStorage.setup(); + + // https://github.com/flutter/flutter/blob/1d60fc784359079041de26bf999e6715aea442f0/packages/flutter/test/material/icons_test.dart#L152 + for (final (name, fontFamily) in [ + (defaultFontFamily, defaultFontFamily), + ('NotoSansMono-Regular', 'monospace'), + ]) { + final fontFile = File('test/fonts/$name.ttf'); + final bytes = Future.value(fontFile.readAsBytesSync().buffer.asByteData()); + + await (FontLoader(fontFamily)..addFont(bytes)).load(); + } + }); + + void testMarkdown( + String name, + String text, { + BuiltMap>? parameters, + void Function(String)? onReferenceClicked, + Future Function(WidgetTester tester)? body, + }) { + testWidgets(name, (tester) async { + late TextSpan span; + + await tester.pumpWidgetWithAccessibility( + MaterialApp( + theme: ThemeData( + fontFamily: defaultFontFamily, + ), + home: Scaffold( + body: Builder( + builder: (context) { + final style = Theme.of(context).textTheme.bodyMedium!; + span = buildRichTextSpan( + account: MockAccount(), + text: text, + textStyle: style, + parameters: parameters ?? BuiltMap(), + references: BuiltList(), + onReferenceClicked: onReferenceClicked ?? (_) {}, + isMarkdown: true, + ); + + return RichText( + text: span, + ); + }, + ), + ), + ), + ); + await tester.pumpAndSettle(); + await expectLater( + find.byType(Scaffold).first, + matchesGoldenFile('goldens/rich_text_${Uri.encodeComponent(name.replaceAll(' ', '_'))}.png'), + ); + + await body?.call(tester); + }); + } + + // These features are not tested in the nextcloud-vue library, but they do work... + group('custom', () { + testMarkdown( + 'subscript', + 'text[^first]\n[^first]: footnote', + ); + + testMarkdown( + 'nested lists', + ''' +- a + - b + - c +- d + - e''', + ); + + // Use a data URI to avoid loading an image over the network + testMarkdown( + 'image', + '![alt](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAABhGlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV9TpUUqDnYQcQhYneyiIrppFYpQIdQKrTqYXPoFTRqSFBdHwbXg4Mdi1cHFWVcHV0EQ/ABxdnBSdJES/5cUWsR4cNyPd/ced+8AoVFhmtU1C2i6baaTCTGbWxVDr4ggjChmMCwzy5iTpBR8x9c9Any9i/Ms/3N/jl41bzEgIBLPMsO0iTeIpzZtg/M+cZSVZJX4nHjMpAsSP3Jd8fiNc9FlgWdGzUx6njhKLBY7WOlgVjI14knimKrplC9kPVY5b3HWKjXWuid/YSSvryxzneYQkljEEiSIUFBDGRXYiNOqk2IhTfsJH/+g65fIpZCrDEaOBVShQXb94H/wu1urMDHuJUUSQPeL43yMAKFdoFl3nO9jx2meAMFn4Epv+6sNYPqT9Hpbix0BfdvAxXVbU/aAyx1g4MmQTdmVgjSFQgF4P6NvygH9t0DPmtdbax+nD0CGukrdAAeHwGiRstd93h3u7O3fM63+fgDncXLVXR+lTAAAAAlwSFlzAAAuIwAALiMBeKU/dgAAAAd0SU1FB+gMCBUAH6Fims8AAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAnklEQVR42u3QMQEAAAgDILV/51nBzwci0CmuRoEsWbJkyZKlQJYsWbJkyVIgS5YsWbJkKZAlS5YsWbIUyJIlS5YsWQpkyZIlS5YsBbJkyZIlS5YCWbJkyZIlS4EsWbJkyZKlQJYsWbJkyVIgS5YsWbJkKZAlS5YsWbIUyJIlS5YsWQpkyZIlS5YsBbJkyZIlS5YCWbJkyZIlS4EsWd8Wil4Bx2r6t7cAAAAASUVORK5CYII=)', + body: (tester) async { + expect(find.byTooltip('alt'), findsOne); + }, + ); + + testMarkdown( + 'rich object', + '# *abc {object} def*', + parameters: BuiltMap({ + 'object': BuiltMap({ + 'id': JsonObject('object'), + // Use an unimplemented type to trigger the simple fallback case + 'type': JsonObject('email'), + 'name': JsonObject('object'), + }), + }), + ); + + group('reference', () { + final callback = MockOnReferenceClickedCallback(); + + testMarkdown( + 'reference plain', + 'abc https://example.com def', + onReferenceClicked: callback.call, + body: (tester) async { + await tester.tap(find.textContaining('https://example.com', findRichText: true)); + await tester.pumpAndSettle(); + + verify(() => callback('https://example.com')).called(1); + }, + ); + + testMarkdown( + 'reference embedded', + 'abc [text](https://example.com) def', + onReferenceClicked: callback.call, + body: (tester) async { + await tester.tap(find.textContaining('text', findRichText: true)); + await tester.pumpAndSettle(); + + verify(() => callback('https://example.com')).called(1); + }, + ); + }); + }); + + // https://github.com/nextcloud-libraries/nextcloud-vue/blob/master/cypress/component/richtext.cy.ts + group('nextcloud-vue', () { + group('normal text', () { + testMarkdown( + 'XML-like text (escaped and unescaped)', + 'text</span>', + ); + }); + + group('headings', () { + testMarkdown( + 'heading (with hash (#) syntax divided with space from text)', + ''' +# heading 1 +## heading 2 +### heading 3 +#### heading 4 +##### heading 5 +###### heading 6''', + ); + + testMarkdown( + 'ignored heading (with hash (#) syntax padded to the text)', + '#heading', + ); + + testMarkdown( + 'heading 1 (with equal (=) syntax on the next line)', + 'heading 1\n==', + ); + + testMarkdown( + 'heading 2 (with dash (-) syntax on the next line)', + 'heading 2\n--', + ); + }); + + group('bold text', () { + testMarkdown( + 'bold text (single with asterisk syntax)', + '**bold asterisk**', + ); + + testMarkdown( + 'bold text (single with underscore syntax)', + '__bold underscore__', + ); + + testMarkdown( + 'bold text (several in line with different syntax)', + 'normal text __bold underscore__ normal text **bold asterisk** normal text', + ); + + testMarkdown( + 'bold text (between normal texts with asterisk syntax)', + 'text**bold**text', + ); + + testMarkdown( + 'ignored bold text (between normal texts with underscore syntax)', + 'text__bold__text', + ); + + testMarkdown( + 'normal text (between bold texts with asterisk syntax)', + '**bold asterisk**normal text**bold asterisk**', + ); + }); + + group('italic text', () { + testMarkdown( + 'italic text (single with asterisk syntax)', + '*italic asterisk*', + ); + + testMarkdown( + 'italic text (single with underscore syntax)', + '_italic underscore_', + ); + + testMarkdown( + 'italic text (several in line with different syntax)', + 'normal text _italic underscore_ normal text *italic asterisk* normal text', + ); + + testMarkdown( + 'italic text (between normal texts with asterisk syntax)', + 'text*italic*text', + ); + + testMarkdown( + 'ignored italic text (between normal texts with underscore syntax)', + 'text_italic_text', + ); + + testMarkdown( + 'normal text (between italic texts with asterisk syntax)', + '*italic asterisk*normal text*italic asterisk*', + ); + }); + + group('strikethrough text', () { + // This one is broken and fixed by https://github.com/dart-lang/markdown/pull/595 which is not released yet. + testMarkdown( + 'strikethrough text (with single tilda syntax)', + '~strikethrough single~', + ); + + testMarkdown( + 'strikethrough text (with double tilda syntax)', + '~~strikethrough double~~', + ); + + testMarkdown( + 'strikethrough text (several in line with different syntax)', + 'normal text ~strikethrough single~ normal text ~~strikethrough double~~ normal text', + ); + + testMarkdown( + 'strikethrough text (between normal texts with different syntax)', + 'text~strikethrough~text~~strikethrough~~text', + ); + }); + + group('inline code', () { + testMarkdown( + 'inline code (single with backticks syntax)', + 'normal text `inline code` normal text', + ); + + testMarkdown( + 'inline code (single with double backticks syntax)', + 'normal text ``inline code`` normal text', + ); + + testMarkdown( + 'inline code (single with triple backticks syntax)', + 'normal text ```inline code``` normal text', + ); + + testMarkdown( + 'inline code (several in line)', + 'normal text `inline code 1`normal text ``inline code 2`` normal text', + ); + + testMarkdown( + 'inline code (between normal texts)', + 'text`inline code`text', + ); + + testMarkdown( + 'inline code (with ignored bold, italic, XML-like syntax)', + '`inline code **bold text** _italic text_ text</span>`', + ); + }); + + group('multiline code', () { + testMarkdown( + 'multiline code (with triple backticks syntax)', + '```\nmultiline code\n```', + ); + + testMarkdown( + 'multiline code (ignored info)', + '```vue\nmultiline code\n```', + ); + + testMarkdown( + 'empty multiline code', + '``````', + ); + + testMarkdown( + 'empty multiline code (with new line)', + '```\n```', + ); + + testMarkdown( + 'multiline code (with several lines)', + '```\nline 1\nline 2\nline 3\n```', + ); + + testMarkdown( + 'multiline code (with ignored bold, italic, inline code, XML-like syntax)', + '```\n**bold text**\n_italic text_\n`inline code`\ntext</span>\n```', + ); + }); + + group('blockquote', () { + testMarkdown( + 'blockquote (with greater then (>) syntax - normal)', + '> blockquote', + ); + + testMarkdown( + 'blockquote (with greater then (>) syntax - escaped)', + '> blockquote', + ); + + testMarkdown( + 'blockquote (with bold, italic text, inline code)', + '> blockquote **bold text** _italic text_ `inline code`', + ); + + testMarkdown( + 'blockquote (with several lines)', + '> line 1\nline 2\n line 3', + ); + + testMarkdown( + 'blockquote (divided from normal text)', + 'normal text\n> line 1\nline 2\n\nnormal text', + ); + + testMarkdown( + 'blockquote (with several paragraphs)', + '> line 1\n>\n> line 3', + ); + + testMarkdown( + 'blockquote (with nested blockquote)', + '> blockquote\n>\n>> nested blockquote', + ); + }); + + group('lists', () { + testMarkdown( + 'ordered list (with number + `.` syntax divided with space from text)', + ''' +1. item 1 +2. item 2 +3. item 3''', + ); + + testMarkdown( + 'unordered list (with unite syntax divided with space from text)', + ''' +* item 1 +* item 2 +* item 3''', + ); + + testMarkdown( + 'unordered lists (with different syntax divided with space from text)', + ''' +* item 1 ++ item 2 +- item 3''', + ); + }); + + group('task lists', () { + testMarkdown( + 'task list (with `- [ ]` and `- [x]` syntax divided with space from text)', + ''' +- [ ] item 1 +- [x] item 2 +- [ ] item 3''', + ); + }); + + group('tables', () { + testMarkdown( + 'table (with `-- | --` syntax)', + 'Table | Column A | Column B\n-- | -- | --\nRow 1 | Value A1 | Value B1\nRow 2 | Value A2 | Value B2', + ); + }); + + group('dividers', () { + testMarkdown( + 'dividers (with different syntax)', + '***\n---\n___', + ); + }); + }); + }); }); } diff --git a/packages/neon_framework/packages/notifications_app/lib/src/widgets/notification.dart b/packages/neon_framework/packages/notifications_app/lib/src/widgets/notification.dart index 95479353dc7..59f4d341d3d 100644 --- a/packages/neon_framework/packages/notifications_app/lib/src/widgets/notification.dart +++ b/packages/neon_framework/packages/notifications_app/lib/src/widgets/notification.dart @@ -26,10 +26,12 @@ class NotificationsNotification extends StatelessWidget { final subject = notification.subjectRichParameters!.isNotEmpty ? Text.rich( buildRichTextSpan( + account: NeonProvider.of(context), text: notification.subjectRich!, + isMarkdown: false, parameters: notification.subjectRichParameters!, references: BuiltList(), - style: Theme.of(context).textTheme.bodyLarge!, + textStyle: Theme.of(context).textTheme.bodyLarge!, onReferenceClicked: (_) {}, ), ) @@ -38,10 +40,12 @@ class NotificationsNotification extends StatelessWidget { final message = notification.messageRichParameters!.isNotEmpty ? Text.rich( buildRichTextSpan( + account: NeonProvider.of(context), text: notification.messageRich!, + isMarkdown: false, parameters: notification.messageRichParameters!, references: BuiltList(), - style: Theme.of(context).textTheme.bodyMedium!, + textStyle: Theme.of(context).textTheme.bodyMedium!, onReferenceClicked: (_) {}, ), overflow: TextOverflow.ellipsis, diff --git a/packages/neon_framework/packages/talk_app/lib/src/widgets/message.dart b/packages/neon_framework/packages/talk_app/lib/src/widgets/message.dart index 05c1da8654b..13a09508f13 100644 --- a/packages/neon_framework/packages/talk_app/lib/src/widgets/message.dart +++ b/packages/neon_framework/packages/talk_app/lib/src/widgets/message.dart @@ -84,11 +84,13 @@ class TalkMessagePreview extends StatelessWidget { ), ), buildRichTextSpan( + account: NeonProvider.of(context), text: chatMessage.message, + isMarkdown: false, parameters: chatMessage.messageParameters, references: BuiltList(), isPreview: true, - style: Theme.of(context).textTheme.bodyMedium!, + textStyle: Theme.of(context).textTheme.bodyMedium!, onReferenceClicked: (url) async => launchUrl(NeonProvider.of(context), url), ), ], @@ -175,10 +177,12 @@ class TalkSystemMessage extends StatelessWidget { child: Center( child: RichText( text: buildRichTextSpan( + account: NeonProvider.of(context), text: chatMessage.message, + isMarkdown: chatMessage.markdown, parameters: chatMessage.messageParameters, references: BuiltList(), - style: Theme.of(context).textTheme.labelSmall!, + textStyle: Theme.of(context).textTheme.labelSmall!, onReferenceClicked: (url) async => launchUrl(NeonProvider.of(context), url), ), ), @@ -378,11 +382,13 @@ class _TalkCommentMessageState extends State { Widget text = Text.rich( buildRichTextSpan( + account: NeonProvider.of(context), text: widget.chatMessage.message, + isMarkdown: widget.chatMessage.markdown && !widget.isParent, parameters: widget.chatMessage.messageParameters, isPreview: widget.isParent, references: references.keys.toBuiltList(), - style: textTheme.bodyLarge!.copyWith( + textStyle: textTheme.bodyLarge!.copyWith( color: widget.isParent || widget.chatMessage.messageType == spreed.MessageType.commentDeleted ? labelColor : null, diff --git a/packages/neon_framework/packages/talk_app/test/goldens/message_comment_message_with_markdown.png b/packages/neon_framework/packages/talk_app/test/goldens/message_comment_message_with_markdown.png new file mode 100644 index 00000000000..81fca0924de Binary files /dev/null and b/packages/neon_framework/packages/talk_app/test/goldens/message_comment_message_with_markdown.png differ diff --git a/packages/neon_framework/packages/talk_app/test/message_input_test.dart b/packages/neon_framework/packages/talk_app/test/message_input_test.dart index 34a34be64aa..59b453f8f1f 100644 --- a/packages/neon_framework/packages/talk_app/test/message_input_test.dart +++ b/packages/neon_framework/packages/talk_app/test/message_input_test.dart @@ -279,6 +279,7 @@ void main() { when(() => chatMessage.actorDisplayName).thenReturn('test'); when(() => chatMessage.message).thenReturn('message'); when(() => chatMessage.messageParameters).thenReturn(BuiltMap()); + when(() => chatMessage.markdown).thenReturn(false); replyTo.add(chatMessage); await tester.pumpAndSettle(); @@ -315,6 +316,7 @@ void main() { when(() => chatMessage.actorDisplayName).thenReturn('test'); when(() => chatMessage.message).thenReturn('message'); when(() => chatMessage.messageParameters).thenReturn(BuiltMap()); + when(() => chatMessage.markdown).thenReturn(false); editing.add(chatMessage); await tester.pumpAndSettle(); diff --git a/packages/neon_framework/packages/talk_app/test/message_test.dart b/packages/neon_framework/packages/talk_app/test/message_test.dart index e2eed60d849..d3745151b2e 100644 --- a/packages/neon_framework/packages/talk_app/test/message_test.dart +++ b/packages/neon_framework/packages/talk_app/test/message_test.dart @@ -79,6 +79,7 @@ core.OcsGetCapabilitiesResponseApplicationJson_Ocs_Data buildCapabilities(core.S ); void main() { + late Account account; late spreed.Room room; late ReferencesBloc referencesBloc; late CapabilitiesBloc capabilitiesBloc; @@ -94,6 +95,8 @@ void main() { }); setUp(() { + account = MockAccount(); + room = MockRoom(); when(() => room.readOnly).thenReturn(0); when(() => room.permissions).thenReturn(spreed.ParticipantPermission.canSendMessageAndShareAndReact.binary); @@ -181,6 +184,9 @@ void main() { await tester.pumpWidgetWithAccessibility( wrapWidget( + providers: [ + Provider.value(value: account), + ], child: TalkMessagePreview( actorId: 'test', roomType: spreed.RoomType.group, @@ -201,6 +207,9 @@ void main() { await tester.pumpWidgetWithAccessibility( wrapWidget( + providers: [ + Provider.value(value: account), + ], child: TalkMessagePreview( actorId: 'abc', roomType: spreed.RoomType.group, @@ -220,6 +229,9 @@ void main() { await tester.pumpWidgetWithAccessibility( wrapWidget( + providers: [ + Provider.value(value: account), + ], child: TalkMessagePreview( actorId: 'test', roomType: spreed.RoomType.oneToOne, @@ -239,6 +251,9 @@ void main() { await tester.pumpWidgetWithAccessibility( wrapWidget( + providers: [ + Provider.value(value: account), + ], child: TalkMessagePreview( actorId: 'abc', roomType: spreed.RoomType.oneToOne, @@ -258,6 +273,9 @@ void main() { await tester.pumpWidgetWithAccessibility( wrapWidget( + providers: [ + Provider.value(value: account), + ], child: TalkMessagePreview( actorId: 'abc', roomType: spreed.RoomType.group, @@ -277,6 +295,9 @@ void main() { await tester.pumpWidgetWithAccessibility( wrapWidget( + providers: [ + Provider.value(value: account), + ], child: TalkMessagePreview( actorId: 'abc', roomType: spreed.RoomType.oneToOne, @@ -295,9 +316,13 @@ void main() { when(() => chatMessage.systemMessage).thenReturn(''); when(() => chatMessage.message).thenReturn(''); when(() => chatMessage.messageParameters).thenReturn(BuiltMap()); + when(() => chatMessage.markdown).thenReturn(false); await tester.pumpWidgetWithAccessibility( wrapWidget( + providers: [ + Provider.value(value: account), + ], child: TalkMessage( room: room, chatMessage: chatMessage, @@ -309,8 +334,6 @@ void main() { }); testWidgets('Comment', (tester) async { - final account = MockAccount(); - final chatMessage = MockChatMessage(); when(() => chatMessage.messageType).thenReturn(spreed.MessageType.comment); when(() => chatMessage.timestamp).thenReturn(0); @@ -321,6 +344,7 @@ void main() { when(() => chatMessage.reactions).thenReturn(BuiltMap()); when(() => chatMessage.messageParameters).thenReturn(BuiltMap()); when(() => chatMessage.isReplyable).thenReturn(true); + when(() => chatMessage.markdown).thenReturn(false); final roomBloc = MockRoomBloc(); when(() => roomBloc.reactions).thenAnswer((_) => BehaviorSubject.seeded(BuiltMap())); @@ -350,9 +374,13 @@ void main() { when(() => chatMessage.systemMessage).thenReturn(''); when(() => chatMessage.message).thenReturn('test'); when(() => chatMessage.messageParameters).thenReturn(BuiltMap()); + when(() => chatMessage.markdown).thenReturn(false); await tester.pumpWidgetWithAccessibility( wrapWidget( + providers: [ + Provider.value(value: account), + ], child: TalkSystemMessage( chatMessage: chatMessage, previousChatMessage: null, @@ -372,9 +400,13 @@ void main() { when(() => chatMessage.systemMessage).thenReturn(''); when(() => chatMessage.message).thenReturn('test'); when(() => chatMessage.messageParameters).thenReturn(BuiltMap()); + when(() => chatMessage.markdown).thenReturn(false); await tester.pumpWidgetWithAccessibility( wrapWidget( + providers: [ + Provider.value(value: account), + ], child: TalkSystemMessage( chatMessage: chatMessage, previousChatMessage: previousChatMessage, @@ -391,8 +423,6 @@ void main() { }); testWidgets('TalkParentMessage', (tester) async { - final account = MockAccount(); - final chatMessage = MockChatMessage(); when(() => chatMessage.messageType).thenReturn(spreed.MessageType.comment); when(() => chatMessage.timestamp).thenReturn(0); @@ -402,6 +432,7 @@ void main() { when(() => chatMessage.message).thenReturn('abc'); when(() => chatMessage.reactions).thenReturn(BuiltMap()); when(() => chatMessage.messageParameters).thenReturn(BuiltMap()); + when(() => chatMessage.markdown).thenReturn(false); await tester.pumpWidgetWithAccessibility( wrapWidget( @@ -440,6 +471,7 @@ void main() { when(() => chatMessage.messageParameters).thenReturn(BuiltMap()); when(() => chatMessage.id).thenReturn(0); when(() => chatMessage.isReplyable).thenReturn(true); + when(() => chatMessage.markdown).thenReturn(false); final roomBloc = MockRoomBloc(); when(() => roomBloc.reactions).thenAnswer((_) => BehaviorSubject.seeded(BuiltMap())); @@ -474,8 +506,6 @@ void main() { }); testWidgets('Other', (tester) async { - final account = MockAccount(); - final previousChatMessage = MockChatMessage(); when(() => previousChatMessage.messageType).thenReturn(spreed.MessageType.comment); when(() => previousChatMessage.timestamp).thenReturn(0); @@ -492,6 +522,7 @@ void main() { when(() => chatMessage.messageParameters).thenReturn(BuiltMap()); when(() => chatMessage.id).thenReturn(0); when(() => chatMessage.isReplyable).thenReturn(true); + when(() => chatMessage.markdown).thenReturn(false); final roomBloc = MockRoomBloc(); when(() => roomBloc.reactions).thenAnswer((_) => BehaviorSubject.seeded(BuiltMap())); @@ -526,8 +557,6 @@ void main() { }); testWidgets('Deleted', (tester) async { - final account = MockAccount(); - final previousChatMessage = MockChatMessage(); when(() => previousChatMessage.messageType).thenReturn(spreed.MessageType.comment); when(() => previousChatMessage.timestamp).thenReturn(0); @@ -543,6 +572,7 @@ void main() { when(() => chatMessage.reactions).thenReturn(BuiltMap()); when(() => chatMessage.messageParameters).thenReturn(BuiltMap()); when(() => chatMessage.isReplyable).thenReturn(true); + when(() => chatMessage.markdown).thenReturn(false); await tester.pumpWidgetWithAccessibility( wrapWidget( @@ -569,8 +599,6 @@ void main() { }); testWidgets('As parent', (tester) async { - final account = MockAccount(); - final chatMessage = MockChatMessage(); when(() => chatMessage.timestamp).thenReturn(0); when(() => chatMessage.actorId).thenReturn('test'); @@ -578,6 +606,7 @@ void main() { when(() => chatMessage.messageType).thenReturn(spreed.MessageType.comment); when(() => chatMessage.message).thenReturn('abc'); when(() => chatMessage.messageParameters).thenReturn(BuiltMap()); + when(() => chatMessage.markdown).thenReturn(false); await tester.pumpWidgetWithAccessibility( wrapWidget( @@ -606,8 +635,6 @@ void main() { }); testWidgets('With parent', (tester) async { - final account = MockAccount(); - final previousChatMessage = MockChatMessage(); when(() => previousChatMessage.messageType).thenReturn(spreed.MessageType.comment); when(() => previousChatMessage.timestamp).thenReturn(0); @@ -620,6 +647,7 @@ void main() { when(() => parentChatMessage.messageType).thenReturn(spreed.MessageType.comment); when(() => parentChatMessage.message).thenReturn('abc'); when(() => parentChatMessage.messageParameters).thenReturn(BuiltMap()); + when(() => parentChatMessage.markdown).thenReturn(false); final chatMessage = MockChatMessageWithParent(); when(() => chatMessage.timestamp).thenReturn(0); @@ -632,6 +660,7 @@ void main() { when(() => chatMessage.parent).thenReturn((chatMessage: parentChatMessage, deletedChatMessage: null)); when(() => chatMessage.messageParameters).thenReturn(BuiltMap()); when(() => chatMessage.isReplyable).thenReturn(true); + when(() => chatMessage.markdown).thenReturn(false); final roomBloc = MockRoomBloc(); when(() => roomBloc.reactions).thenAnswer((_) => BehaviorSubject.seeded(BuiltMap())); @@ -689,8 +718,6 @@ void main() { ), ); - final account = MockAccount(); - final chatMessage = MockChatMessageWithParent(); when(() => chatMessage.id).thenReturn(0); when(() => chatMessage.timestamp).thenReturn(0); @@ -702,6 +729,7 @@ void main() { when(() => chatMessage.reactions).thenReturn(BuiltMap()); when(() => chatMessage.messageParameters).thenReturn(BuiltMap()); when(() => chatMessage.isReplyable).thenReturn(true); + when(() => chatMessage.markdown).thenReturn(false); final roomBloc = MockRoomBloc(); when(() => roomBloc.reactions).thenAnswer((_) => BehaviorSubject.seeded(BuiltMap())); @@ -748,10 +776,53 @@ void main() { ); }); + testWidgets('With markdown', (tester) async { + final chatMessage = MockChatMessageWithParent(); + when(() => chatMessage.id).thenReturn(0); + when(() => chatMessage.timestamp).thenReturn(0); + when(() => chatMessage.actorId).thenReturn('test'); + when(() => chatMessage.actorType).thenReturn(spreed.ActorType.users); + when(() => chatMessage.actorDisplayName).thenReturn('test'); + when(() => chatMessage.messageType).thenReturn(spreed.MessageType.comment); + when(() => chatMessage.message).thenReturn(''' +# abc + +def +'''); + when(() => chatMessage.reactions).thenReturn(BuiltMap()); + when(() => chatMessage.messageParameters).thenReturn(BuiltMap()); + when(() => chatMessage.isReplyable).thenReturn(true); + when(() => chatMessage.markdown).thenReturn(true); + + final roomBloc = MockRoomBloc(); + when(() => roomBloc.reactions).thenAnswer((_) => BehaviorSubject.seeded(BuiltMap())); + + await tester.pumpWidgetWithAccessibility( + wrapWidget( + providers: [ + Provider.value(value: account), + NeonProvider.value(value: roomBloc), + NeonProvider.value(value: referencesBloc), + NeonProvider.value(value: capabilitiesBloc), + ], + child: TalkCommentMessage( + room: room, + chatMessage: chatMessage, + lastCommonRead: null, + ), + ), + ); + + await tester.pumpAndSettle(); + + await expectLater( + find.byType(TalkCommentMessage).first, + matchesGoldenFile('goldens/message_comment_message_with_markdown.png'), + ); + }); + group('Separate messages', () { testWidgets('Actor', (tester) async { - final account = MockAccount(); - final previousChatMessage = MockChatMessage(); when(() => previousChatMessage.messageType).thenReturn(spreed.MessageType.comment); when(() => previousChatMessage.timestamp).thenReturn(0); @@ -767,6 +838,7 @@ void main() { when(() => chatMessage.reactions).thenReturn(BuiltMap({'๐Ÿ˜€': 1, '๐Ÿ˜Š': 23})); when(() => chatMessage.messageParameters).thenReturn(BuiltMap()); when(() => chatMessage.isReplyable).thenReturn(true); + when(() => chatMessage.markdown).thenReturn(false); final roomBloc = MockRoomBloc(); when(() => roomBloc.reactions).thenAnswer((_) => BehaviorSubject.seeded(BuiltMap())); @@ -800,8 +872,6 @@ void main() { }); testWidgets('Time', (tester) async { - final account = MockAccount(); - final previousChatMessage = MockChatMessage(); when(() => previousChatMessage.messageType).thenReturn(spreed.MessageType.comment); when(() => previousChatMessage.timestamp).thenReturn(0); @@ -817,6 +887,7 @@ void main() { when(() => chatMessage.reactions).thenReturn(BuiltMap({'๐Ÿ˜€': 1, '๐Ÿ˜Š': 23})); when(() => chatMessage.messageParameters).thenReturn(BuiltMap()); when(() => chatMessage.isReplyable).thenReturn(true); + when(() => chatMessage.markdown).thenReturn(false); final roomBloc = MockRoomBloc(); when(() => roomBloc.reactions).thenAnswer((_) => BehaviorSubject.seeded(BuiltMap())); @@ -849,8 +920,6 @@ void main() { }); testWidgets('System message', (tester) async { - final account = MockAccount(); - final previousChatMessage = MockChatMessage(); when(() => previousChatMessage.messageType).thenReturn(spreed.MessageType.system); when(() => previousChatMessage.timestamp).thenReturn(0); @@ -866,6 +935,7 @@ void main() { when(() => chatMessage.reactions).thenReturn(BuiltMap({'๐Ÿ˜€': 1, '๐Ÿ˜Š': 23})); when(() => chatMessage.messageParameters).thenReturn(BuiltMap()); when(() => chatMessage.isReplyable).thenReturn(true); + when(() => chatMessage.markdown).thenReturn(false); final roomBloc = MockRoomBloc(); when(() => roomBloc.reactions).thenAnswer((_) => BehaviorSubject.seeded(BuiltMap())); @@ -899,8 +969,6 @@ void main() { }); testWidgets('Edited', (tester) async { - final account = MockAccount(); - final previousChatMessage = MockChatMessage(); when(() => previousChatMessage.messageType).thenReturn(spreed.MessageType.comment); when(() => previousChatMessage.timestamp).thenReturn(0); @@ -918,6 +986,7 @@ void main() { when(() => chatMessage.lastEditTimestamp).thenReturn(0); when(() => chatMessage.lastEditActorDisplayName).thenReturn('test'); when(() => chatMessage.isReplyable).thenReturn(true); + when(() => chatMessage.markdown).thenReturn(false); final roomBloc = MockRoomBloc(); when(() => roomBloc.reactions).thenAnswer((_) => BehaviorSubject.seeded(BuiltMap())); @@ -972,6 +1041,7 @@ void main() { when(() => chatMessage.messageParameters).thenReturn(BuiltMap()); when(() => chatMessage.id).thenReturn(0); when(() => chatMessage.isReplyable).thenReturn(true); + when(() => chatMessage.markdown).thenReturn(false); roomBloc = MockRoomBloc(); when(() => roomBloc.reactions).thenAnswer((_) => BehaviorSubject.seeded(BuiltMap())); diff --git a/packages/neon_framework/packages/talk_app/test/room_page_test.dart b/packages/neon_framework/packages/talk_app/test/room_page_test.dart index 114be2dba46..f940345bc80 100644 --- a/packages/neon_framework/packages/talk_app/test/room_page_test.dart +++ b/packages/neon_framework/packages/talk_app/test/room_page_test.dart @@ -121,6 +121,7 @@ void main() { when(() => chatMessage1.messageParameters).thenReturn(BuiltMap()); when(() => chatMessage1.systemMessage).thenReturn(''); when(() => chatMessage1.isReplyable).thenReturn(true); + when(() => chatMessage1.markdown).thenReturn(false); final chatMessage2 = MockChatMessageWithParent(); when(() => chatMessage2.id).thenReturn(2); @@ -134,6 +135,7 @@ void main() { when(() => chatMessage2.messageParameters).thenReturn(BuiltMap()); when(() => chatMessage2.systemMessage).thenReturn(''); when(() => chatMessage2.isReplyable).thenReturn(true); + when(() => chatMessage2.markdown).thenReturn(false); final chatMessage3 = MockChatMessageWithParent(); when(() => chatMessage3.id).thenReturn(3); @@ -147,6 +149,7 @@ void main() { when(() => chatMessage3.messageParameters).thenReturn(BuiltMap()); when(() => chatMessage3.systemMessage).thenReturn(''); when(() => chatMessage3.isReplyable).thenReturn(true); + when(() => chatMessage3.markdown).thenReturn(false); when(() => bloc.messages).thenAnswer( (_) => BehaviorSubject.seeded( diff --git a/packages/neon_framework/pubspec.yaml b/packages/neon_framework/pubspec.yaml index 1edc53171f2..bb00ca4e48a 100644 --- a/packages/neon_framework/pubspec.yaml +++ b/packages/neon_framework/pubspec.yaml @@ -44,6 +44,7 @@ dependencies: intersperse: ^2.0.0 intl: ^0.19.0 logging: ^1.0.0 + markdown: ^7.0.0 meta: ^1.0.0 neon_http_client: git: