diff --git a/lib/pages/chat/events/html_message.dart b/lib/pages/chat/events/html_message.dart index 055f6b9f9..75ed9ffb4 100644 --- a/lib/pages/chat/events/html_message.dart +++ b/lib/pages/chat/events/html_message.dart @@ -162,65 +162,68 @@ class HtmlMessage extends StatelessWidget { String fullHtml, List remainingTokens, ) { - for (final node in element.nodes) { - node.replaceWith(_tokenizeHtml(node, fullHtml, remainingTokens)); - } + final regex = RegExp(r'(<[^>]+>)'); - if (element is dom.Text) { - // once a text element in reached in the HTML tree, find and - // wrap all the spans with matching tokens until all tokens - // have been wrapped or no more text elements remain - String tokenizedText = element.text; - while (remainingTokens.isNotEmpty) { - final tokenText = remainingTokens.first.text.content; + final matches = regex.allMatches(fullHtml); + final List result = []; + int lastEnd = 0; - int startIndex = tokenizedText.lastIndexOf(''); - startIndex = startIndex == -1 ? 0 : startIndex + 8; - final int tokenIndex = tokenizedText.indexOf( - tokenText, - startIndex, - ); - - // if the token is not found in the text, check if the token exist in the full HTML. - // If not, remove the token and continue. If so, break to move on to the next node in the HTML. - if (tokenIndex == -1) { - final fullHtmlIndex = fullHtml.indexOf(tokenText); - if (fullHtmlIndex == -1) { - remainingTokens.removeAt(0); - continue; - } else { - break; - } - } - - final token = remainingTokens.removeAt(0); - final tokenEnd = tokenIndex + tokenText.length; - final before = tokenizedText.substring(0, tokenIndex); - final after = tokenizedText.substring(tokenEnd); - - tokenizedText = - "$before$tokenText$after"; + for (final match in matches) { + if (match.start > lastEnd) { + result.add(fullHtml.substring(lastEnd, match.start)); // Text before tag } - - final newElement = dom.Element.html('$tokenizedText'); - return newElement; + result.add(match.group(0)!); // The tag itself + lastEnd = match.end; } - return element; + if (lastEnd < fullHtml.length) { + result.add(fullHtml.substring(lastEnd)); // Remaining text after last tag + } + + for (final PangeaToken token in tokens ?? []) { + final String tokenText = token.text.content; + final substringIndex = result.indexWhere( + (string) => + string.contains(tokenText) && + !(string.startsWith('<') && string.endsWith('>')), + ); + + if (substringIndex == -1) continue; + final int tokenIndex = result[substringIndex].indexOf(tokenText); + if (tokenIndex == -1) continue; + + final int tokenLength = tokenText.characters.length; + final before = result[substringIndex].substring(0, tokenIndex); + final after = result[substringIndex].substring(tokenIndex + tokenLength); + result.replaceRange(substringIndex, substringIndex + 1, [ + if (before.isNotEmpty) before, + '$tokenText', + if (after.isNotEmpty) after, + ]); + } + + return dom.Element.html('${result.join()}'); } // Pangea# /// Adding line breaks before block elements. List _renderWithLineBreaks( dom.NodeList nodes, - BuildContext context, { + // #Pangea + // BuildContext context, { + BuildContext context, + TextStyle textStyle, { + // Pangea# int depth = 1, }) { final onlyElements = nodes.whereType().toList(); return [ for (var i = 0; i < nodes.length; i++) ...[ // Actually render the node child: - _renderHtml(nodes[i], context, depth: depth + 1), + // #Pangea + // _renderHtml(nodes[i], context, depth: depth + 1), + _renderHtml(nodes[i], context, textStyle, depth: depth + 1), + // Pangea# // Add linebreaks between blocks: if (nodes[i] is dom.Element && onlyElements.indexOf(nodes[i] as dom.Element) < @@ -237,7 +240,11 @@ class HtmlMessage extends StatelessWidget { /// Transforms a Node to an InlineSpan. InlineSpan _renderHtml( dom.Node node, - BuildContext context, { + // #Pangea + // BuildContext context, { + BuildContext context, + TextStyle textStyle, { + // Pangea# int depth = 1, }) { // We must not render elements nested more than 100 elements deep: @@ -276,9 +283,11 @@ class HtmlMessage extends StatelessWidget { final renderer = TokenRenderingUtil( pangeaMessageEvent: pangeaMessageEvent, readingAssistanceMode: readingAssistanceMode, - existingStyle: AppConfig.messageTextStyle( - pangeaMessageEvent!.event, - textColor, + existingStyle: textStyle.merge( + AppConfig.messageTextStyle( + pangeaMessageEvent!.event, + textColor, + ), ), overlayController: overlayController, isTransitionAnimation: isTransitionAnimation, @@ -418,6 +427,11 @@ class HtmlMessage extends StatelessWidget { children: _renderWithLineBreaks( node.nodes, context, + // #Pangea + textStyle.merge( + linkStyle.copyWith(height: 1.25), + ), + // Pangea# depth: depth, ), style: linkStyle, @@ -450,6 +464,9 @@ class HtmlMessage extends StatelessWidget { ..._renderWithLineBreaks( node.nodes, context, + // #Pangea + textStyle, + // Pangea# depth: depth, ), ], @@ -478,6 +495,9 @@ class HtmlMessage extends StatelessWidget { children: _renderWithLineBreaks( node.nodes, context, + // #Pangea + textStyle.copyWith(fontStyle: FontStyle.italic), + // Pangea# depth: depth, ), ), @@ -576,12 +596,28 @@ class HtmlMessage extends StatelessWidget { node.localName == 'summary', ) .map( - (node) => _renderHtml(node, context, depth: depth), + // #Pangea + // (node) => _renderHtml(node, context, depth: depth), + (node) => _renderHtml( + node, + context, + textStyle.merge( + TextStyle( + fontSize: fontSize, + color: textColor, + ), + ), + depth: depth, + ), + // Pangea# ) else ..._renderWithLineBreaks( node.nodes, context, + // #Pangea + textStyle, + // Pangea# depth: depth, ), ], @@ -614,6 +650,11 @@ class HtmlMessage extends StatelessWidget { children: _renderWithLineBreaks( node.nodes, context, + // #Pangea + textStyle.copyWith( + backgroundColor: obscure ? textColor : null, + ), + // Pangea# depth: depth, ), ), @@ -628,6 +669,36 @@ class HtmlMessage extends StatelessWidget { ); block: default: + // #Pangea + final style = switch (node.localName) { + 'body' => TextStyle( + fontSize: fontSize, + color: textColor, + ), + 'a' => linkStyle, + 'strong' => const TextStyle(fontWeight: FontWeight.bold), + 'em' || 'i' => const TextStyle(fontStyle: FontStyle.italic), + 'del' || + 'strikethrough' => + const TextStyle(decoration: TextDecoration.lineThrough), + 'u' => const TextStyle(decoration: TextDecoration.underline), + 'h1' => TextStyle(fontSize: fontSize * 1.6, height: 2), + 'h2' => TextStyle(fontSize: fontSize * 1.5, height: 2), + 'h3' => TextStyle(fontSize: fontSize * 1.4, height: 2), + 'h4' => TextStyle(fontSize: fontSize * 1.3, height: 1.75), + 'h5' => TextStyle(fontSize: fontSize * 1.2, height: 1.75), + 'h6' => TextStyle(fontSize: fontSize * 1.1, height: 1.5), + 'span' => TextStyle( + color: node.attributes['color']?.hexToColor ?? + node.attributes['data-mx-color']?.hexToColor ?? + textColor, + backgroundColor: node.attributes['data-mx-bg-color']?.hexToColor, + ), + 'sup' => const TextStyle(fontFeatures: [FontFeature.superscripts()]), + 'sub' => const TextStyle(fontFeatures: [FontFeature.subscripts()]), + _ => null, + }; + // Pangea# return TextSpan( style: switch (node.localName) { 'body' => TextStyle( @@ -663,6 +734,9 @@ class HtmlMessage extends StatelessWidget { children: _renderWithLineBreaks( node.nodes, context, + // #Pangea + textStyle.merge(style ?? const TextStyle()), + // Pangea# depth: depth, ), ); @@ -698,6 +772,12 @@ class HtmlMessage extends StatelessWidget { parsed, // Pangea# context, + // #Pangea + TextStyle( + fontSize: fontSize, + color: textColor, + ), + // Pangea# ), style: TextStyle( fontSize: fontSize,