TF-1961 Transform html email content in composer

(cherry picked from commit f5470c130744242195b265a0ea4aad044ad8e740)
This commit is contained in:
dab246
2023-08-18 19:42:04 +07:00
committed by Dat Vu
parent e06d35f1e1
commit 8eed49fd73
45 changed files with 1139 additions and 644 deletions
@@ -1,7 +1,9 @@
import 'dart:convert';
/// Transforms plain text messages.
abstract class TextTransformer {
const TextTransformer();
String process(String text);
String process(String text, HtmlEscape htmlEscape);
}
@@ -12,7 +12,9 @@ import 'package:html/dom.dart';
class ImageTransformer extends DomTransformer {
const ImageTransformer();
final bool useLoadingAttribute;
const ImageTransformer({this.useLoadingAttribute = false});
@override
Future<void> process({
@@ -40,14 +42,19 @@ class ImageTransformer extends DomTransformer {
);
imageElement.attributes['src'] = imageBase64 ?? src;
} else if (src.startsWith('https://') || src.startsWith('http://')) {
final classAttribute = imageElement.attributes['class'];
if (classAttribute != null) {
imageElement.attributes['class'] = '$classAttribute lazy-loading';
if (useLoadingAttribute) {
imageElement.attributes['loading'] = 'lazy';
} else {
imageElement.attributes['class'] = 'lazy-loading';
final classAttribute = imageElement.attributes['class'];
if (classAttribute != null) {
imageElement.attributes['class'] = '$classAttribute lazy-loading';
} else {
imageElement.attributes['class'] = 'lazy-loading';
}
imageElement.attributes['data-src'] = src;
imageElement.attributes.remove('src');
imageElement.attributes.remove('loading');
}
imageElement.attributes['data-src'] = src;
imageElement.attributes.remove('src');
}
}));
}
@@ -0,0 +1,35 @@
import 'package:core/data/network/dio_client.dart';
import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart';
import 'package:core/presentation/utils/html_transformer/html_template.dart';
import 'package:core/utils/app_logger.dart';
import 'package:html/dom.dart';
class RemoveTooltipLinkTransformer extends DomTransformer {
const RemoveTooltipLinkTransformer();
@override
Future<void> process({
required Document document,
Map<String, String>? mapUrlDownloadCID,
DioClient? dioClient
}) async {
final linkElements = document.querySelectorAll('a.$nameClassToolTip');
await Future.wait(linkElements.map((linkElement) async {
final classAttribute = linkElement.attributes['class'];
if (classAttribute != null) {
final newClassAttribute = classAttribute.replaceFirst(nameClassToolTip, '');
linkElement.attributes['class'] = newClassAttribute;
}
final listSpanTag = linkElement.querySelectorAll('span.tooltiptext');
log('RemoveTooltipLinkTransformer::process:listSpanTag: ${listSpanTag.length}');
if (listSpanTag.isNotEmpty) {
for (var element in listSpanTag) {
element.remove();
}
}
}));
}
}
@@ -0,0 +1,32 @@
import 'package:core/data/network/dio_client.dart';
import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart';
import 'package:html/dom.dart';
class ReplaceLazyLoadImageTransformer extends DomTransformer {
const ReplaceLazyLoadImageTransformer();
@override
Future<void> process({
required Document document,
Map<String, String>? mapUrlDownloadCID,
DioClient? dioClient
}) async {
final imageElements = document.querySelectorAll('img.lazy-loading');
await Future.wait(imageElements.map((imageElement) async {
final classAttribute = imageElement.attributes['class'];
if (classAttribute != null) {
final newClassAttribute = classAttribute.replaceFirst('lazy-loading', '');
imageElement.attributes['class'] = newClassAttribute;
}
final dataSrc = imageElement.attributes['data-src'];
if (dataSrc != null) {
imageElement.attributes['src'] = dataSrc;
imageElement.attributes.remove('data-src');
}
imageElement.attributes['loading'] = 'lazy';
}));
}
}
@@ -1,23 +1,26 @@
import 'dart:convert';
import 'package:core/core.dart';
import 'package:core/presentation/utils/html_transformer/message_content_transformer.dart';
class HtmlTransform {
final DioClient _dioClient;
final HtmlEscape _htmlEscape;
HtmlTransform(this._dioClient);
HtmlTransform(this._dioClient, this._htmlEscape);
/// Transforms this message to HTML code.
Future<String> transformToHtml({
required String contentHtml,
Map<String, String>? mapUrlDownloadCID,
required String htmlContent,
Map<String, String>? mapCidImageDownloadUrl,
TransformConfiguration? transformConfiguration,
}) async {
transformConfiguration ??= TransformConfiguration.create();
final transformer = MessageContentTransformer(transformConfiguration, _dioClient);
final transformer = MessageContentTransformer(transformConfiguration, _dioClient, _htmlEscape);
final document = await transformer.toDocument(
message: contentHtml,
mapUrlDownloadCID: mapUrlDownloadCID
message: htmlContent,
mapUrlDownloadCID: mapCidImageDownloadUrl
);
return document.outerHtml;
}
@@ -28,7 +31,7 @@ class HtmlTransform {
TransformConfiguration? transformConfiguration
}) {
transformConfiguration ??= TransformConfiguration.create();
final transformer = MessageContentTransformer(transformConfiguration, _dioClient);
final transformer = MessageContentTransformer(transformConfiguration, _dioClient, _htmlEscape);
final message = transformer.toMessage(content);
return message;
}
@@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:core/data/network/dio_client.dart';
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
import 'package:html/dom.dart';
@@ -5,23 +7,28 @@ import 'package:html/parser.dart' show parse;
/// Transforms messages
class MessageContentTransformer {
/// The configuration used for the transformation
final TransformConfiguration configuration;
final DioClient dioClient;
/// The _configuration used for the transformation
final TransformConfiguration _configuration;
final DioClient _dioClient;
final HtmlEscape _htmlEscape;
MessageContentTransformer(this.configuration, this.dioClient);
MessageContentTransformer(
this._configuration,
this._dioClient,
this._htmlEscape
);
Future<void> _transformDocument({
required Document document,
Map<String, String>? mapUrlDownloadCID
}) async {
await Future.wait([
if (configuration.domTransformers.isNotEmpty)
...configuration.domTransformers.map((domTransformer) async =>
if (_configuration.domTransformers.isNotEmpty)
..._configuration.domTransformers.map((domTransformer) async =>
domTransformer.process(
document: document,
mapUrlDownloadCID: mapUrlDownloadCID,
dioClient: dioClient
dioClient: _dioClient
)
)
]);
@@ -40,9 +47,9 @@ class MessageContentTransformer {
}
String _transformMessage(String message) {
if (configuration.textTransformers.isNotEmpty) {
for (var transformer in configuration.textTransformers) {
message = transformer.process(message);
if (_configuration.textTransformers.isNotEmpty) {
for (var transformer in _configuration.textTransformers) {
message = transformer.process(message, _htmlEscape);
}
}
return message;
@@ -6,10 +6,8 @@ import 'package:core/presentation/utils/html_transformer/base/text_transformer.d
class SanitizeAutolinkHtmlTransformers extends TextTransformer {
final HtmlEscape htmlEscape;
SanitizeAutolinkHtmlTransformers(this.htmlEscape);
const SanitizeAutolinkHtmlTransformers();
@override
String process(String text) => SanitizeAutolinkFilter(htmlEscape).process(text);
String process(String text, HtmlEscape htmlEscape) => SanitizeAutolinkFilter(htmlEscape).process(text);
}
@@ -1,12 +1,16 @@
import 'package:core/core.dart';
import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart';
import 'package:core/presentation/utils/html_transformer/base/text_transformer.dart';
import 'package:core/presentation/utils/html_transformer/dom/add_target_blank_in_tag_a_transformers.dart';
import 'package:core/presentation/utils/html_transformer/dom/blockcode_transformers.dart';
import 'package:core/presentation/utils/html_transformer/dom/blockquoted_transformers.dart';
import 'package:core/presentation/utils/html_transformer/dom/image_transformers.dart';
import 'package:core/presentation/utils/html_transformer/dom/remove_tooltip_link_transformers.dart';
import 'package:core/presentation/utils/html_transformer/dom/replace_lazy_load_image_transformer.dart';
import 'package:core/presentation/utils/html_transformer/dom/script_transformers.dart';
import 'package:core/presentation/utils/html_transformer/dom/sigature_transformers.dart';
import 'package:core/presentation/utils/html_transformer/text/sanitize_autolink_html_transformers.dart';
/// Contains the configuration for all transformations.
class TransformConfiguration {
@@ -25,6 +29,45 @@ class TransformConfiguration {
this.textTransformers
);
factory TransformConfiguration.forReplyForwardEmail() => TransformConfiguration.create(
customDomTransformers: [
const ReplaceLazyLoadImageTransformer(),
if (PlatformInfo.isWeb)
const RemoveTooltipLinkTransformer(),
]
);
factory TransformConfiguration.forDraftsEmail() => TransformConfiguration.create(
customDomTransformers: [
const RemoveScriptTransformer(),
const BlockQuotedTransformer(),
const BlockCodeTransformer(),
const AddTargetBlankInTagATransformer(),
const ImageTransformer(useLoadingAttribute: true),
]
);
factory TransformConfiguration.forComposeEmailPlatformWeb() => TransformConfiguration.create(
customDomTransformers: [
const RemoveScriptTransformer(),
const BlockQuotedTransformer(),
const BlockCodeTransformer(),
const AddTargetBlankInTagATransformer(),
const ImageTransformer(useLoadingAttribute: true)
]
);
factory TransformConfiguration.forPreviewEmailPlatformWeb() => TransformConfiguration.create(
customDomTransformers: [
const RemoveScriptTransformer(),
const BlockQuotedTransformer(),
const BlockCodeTransformer(),
const AddTargetBlankInTagATransformer(),
const ImageTransformer(),
const AddTooltipLinkTransformer(),
]
);
/// Provides easy access to a standard configuration that does not block external images.
static const TransformConfiguration standardConfiguration = TransformConfiguration(
standardDomTransformers,
@@ -61,13 +104,7 @@ class TransformConfiguration {
ImageTransformer(),
];
static const List<DomTransformer> domTransformersForDraftEmail = [
RemoveScriptTransformer(),
BlockQuotedTransformer(),
BlockCodeTransformer(),
AddTargetBlankInTagATransformer(),
ImageTransformer(),
static const List<TextTransformer> standardTextTransformers = [
SanitizeAutolinkHtmlTransformers()
];
static const List<TextTransformer> standardTextTransformers = [];
}