TF-1961 Refactor html transformer

(cherry picked from commit 0272e45226ddb12827b7ef4820b00632758c353a)
This commit is contained in:
dab246
2023-08-16 12:02:34 +07:00
committed by Dat Vu
parent 289d5bbbe2
commit 1af33dd20d
14 changed files with 134 additions and 130 deletions
@@ -1,5 +1,5 @@
import 'package:core/core.dart'; import 'package:core/data/network/dio_client.dart';
import 'package:html/dom.dart'; import 'package:html/dom.dart';
/// Transforms the HTML DOM. /// Transforms the HTML DOM.
@@ -10,13 +10,11 @@ abstract class DomTransformer {
/// Uses the `DOM` [document] to transform the `document`. /// Uses the `DOM` [document] to transform the `document`.
/// ///
/// All changes will be visible to subsequent transformers. /// All changes will be visible to subsequent transformers.
Future<void> process( Future<void> process({
Document document, required Document document,
{ Map<String, String>? mapUrlDownloadCID,
Map<String, String>? mapUrlDownloadCID, DioClient? dioClient,
DioClient? dioClient, });
}
);
/// Adds a HEAD element if necessary /// Adds a HEAD element if necessary
void ensureDocumentHeadIsAvailable(Document document) { void ensureDocumentHeadIsAvailable(Document document) {
@@ -6,8 +6,8 @@ class AddTargetBlankInTagATransformer extends DomTransformer {
const AddTargetBlankInTagATransformer(); const AddTargetBlankInTagATransformer();
@override @override
Future<void> process( Future<void> process({
Document document, { required Document document,
Map<String, String>? mapUrlDownloadCID, Map<String, String>? mapUrlDownloadCID,
DioClient? dioClient, DioClient? dioClient,
}) async { }) async {
@@ -1,22 +1,19 @@
import 'package:core/data/network/dio_client.dart'; 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/base/dom_transformer.dart';
import 'package:core/presentation/utils/html_transformer/html_template.dart';
import 'package:html/dom.dart'; import 'package:html/dom.dart';
import '../html_template.dart';
class AddTooltipLinkTransformer extends DomTransformer { class AddTooltipLinkTransformer extends DomTransformer {
const AddTooltipLinkTransformer(); const AddTooltipLinkTransformer();
@override @override
Future<void> process( Future<void> process({
Document document, required Document document,
{ Map<String, String>? mapUrlDownloadCID,
Map<String, String>? mapUrlDownloadCID, DioClient? dioClient
DioClient? dioClient }) async {
}
) async {
final linkElements = document.querySelectorAll('a[href^="http"]'); final linkElements = document.querySelectorAll('a[href^="http"]');
await Future.wait(linkElements.map((linkElement) async { await Future.wait(linkElements.map((linkElement) async {
_addToolTipWhenHoverLink(linkElement); _addToolTipWhenHoverLink(linkElement);
@@ -8,13 +8,11 @@ class BlockCodeTransformer extends DomTransformer {
const BlockCodeTransformer(); const BlockCodeTransformer();
@override @override
Future<void> process( Future<void> process({
Document document, required Document document,
{ Map<String, String>? mapUrlDownloadCID,
Map<String, String>? mapUrlDownloadCID, DioClient? dioClient
DioClient? dioClient }) async {
}
) async {
final codeElements = document.getElementsByTagName('pre'); final codeElements = document.getElementsByTagName('pre');
await Future.wait(codeElements.map((element) async { await Future.wait(codeElements.map((element) async {
element.attributes['style'] = ''' element.attributes['style'] = '''
@@ -8,13 +8,11 @@ class BlockQuotedTransformer extends DomTransformer {
const BlockQuotedTransformer(); const BlockQuotedTransformer();
@override @override
Future<void> process( Future<void> process({
Document document, required Document document,
{ Map<String, String>? mapUrlDownloadCID,
Map<String, String>? mapUrlDownloadCID, DioClient? dioClient
DioClient? dioClient }) async {
}
) async {
final quotedElements = document.getElementsByTagName('blockquote'); final quotedElements = document.getElementsByTagName('blockquote');
await Future.wait(quotedElements.map((quotedElement) async { await Future.wait(quotedElements.map((quotedElement) async {
quotedElement.attributes['style'] = ''' quotedElement.attributes['style'] = '''
@@ -5,7 +5,6 @@ import 'package:core/data/network/dio_client.dart';
import 'package:core/data/utils/compress_file_utils.dart'; import 'package:core/data/utils/compress_file_utils.dart';
import 'package:core/presentation/extensions/html_extension.dart'; import 'package:core/presentation/extensions/html_extension.dart';
import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart'; import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart';
import 'package:core/utils/app_logger.dart';
import 'package:core/utils/platform_info.dart'; import 'package:core/utils/platform_info.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@@ -16,16 +15,12 @@ class ImageTransformer extends DomTransformer {
const ImageTransformer(); const ImageTransformer();
@override @override
Future<void> process( Future<void> process({
Document document, required Document document,
{ Map<String, String>? mapUrlDownloadCID,
Map<String, String>? mapUrlDownloadCID, DioClient? dioClient
DioClient? dioClient }) async {
} final imageElements = document.querySelectorAll('img');
) async {
final compressFileUtils = CompressFileUtils();
final imageElements = document.querySelectorAll('img[src^="cid:"]');
log('ImageTransformer::process(): imageElements: ${imageElements.length}');
await Future.wait(imageElements.map((imageElement) async { await Future.wait(imageElements.map((imageElement) async {
final exStyle = imageElement.attributes['style']; final exStyle = imageElement.attributes['style'];
if (exStyle != null) { if (exStyle != null) {
@@ -34,24 +29,42 @@ class ImageTransformer extends DomTransformer {
imageElement.attributes['style'] = 'display: inline;max-width: 100%;'; imageElement.attributes['style'] = 'display: inline;max-width: 100%;';
} }
final src = imageElement.attributes['src']; final src = imageElement.attributes['src'];
if (src != null) {
log('ImageTransformer::process(): src: $src'); if (src == null) return;
final cid = src.replaceFirst('cid:', '').trim();
final urlDownloadCid = mapUrlDownloadCID?[cid]; if (src.startsWith('cid:') && dioClient != null && mapUrlDownloadCID != null) {
log('ImageTransformer::process(): urlDownloadCid: $urlDownloadCid'); final imageBase64 = await _convertCidToBase64Image(
if (urlDownloadCid?.isNotEmpty == true && dioClient != null) { dioClient: dioClient,
final imgBase64Uri = await loadAsyncNetworkImageToBase64( mapUrlDownloadCID: mapUrlDownloadCID,
dioClient, imageSource: src
compressFileUtils, );
urlDownloadCid!); imageElement.attributes['src'] = imageBase64 ?? src;
if (imgBase64Uri.isNotEmpty) {
imageElement.attributes['src'] = imgBase64Uri;
}
}
} }
})); }));
} }
Future<String?> _convertCidToBase64Image({
required DioClient dioClient,
required Map<String, String> mapUrlDownloadCID,
required String imageSource
}) async {
final cid = imageSource.replaceFirst('cid:', '').trim();
final urlDownloadCid = mapUrlDownloadCID[cid];
if (urlDownloadCid == null || urlDownloadCid.isEmpty) return null;
final compressFileUtils = CompressFileUtils();
final imgBase64Uri = await loadAsyncNetworkImageToBase64(
dioClient,
compressFileUtils,
urlDownloadCid
);
if (imgBase64Uri.isEmpty) return null;
return imgBase64Uri;
}
Future<String> loadAsyncNetworkImageToBase64( Future<String> loadAsyncNetworkImageToBase64(
DioClient dioClient, DioClient dioClient,
CompressFileUtils compressFileUtils, CompressFileUtils compressFileUtils,
@@ -8,13 +8,11 @@ class RemoveScriptTransformer extends DomTransformer {
const RemoveScriptTransformer(); const RemoveScriptTransformer();
@override @override
Future<void> process( Future<void> process({
Document document, required Document document,
{ Map<String, String>? mapUrlDownloadCID,
Map<String, String>? mapUrlDownloadCID, DioClient? dioClient
DioClient? dioClient }) async {
}
) async {
final scriptElements = document.getElementsByTagName('script'); final scriptElements = document.getElementsByTagName('script');
await Future.wait(scriptElements.map((scriptElement) async { await Future.wait(scriptElements.map((scriptElement) async {
scriptElement.remove(); scriptElement.remove();
@@ -8,13 +8,11 @@ class SignatureTransformer extends DomTransformer {
const SignatureTransformer(); const SignatureTransformer();
@override @override
Future<void> process( Future<void> process({
Document document, required Document document,
{ Map<String, String>? mapUrlDownloadCID,
Map<String, String>? mapUrlDownloadCID, DioClient? dioClient
DioClient? dioClient }) async {
}
) async {
final signatureElements = document.querySelectorAll('div.tmail-signature'); final signatureElements = document.querySelectorAll('div.tmail-signature');
await Future.wait(signatureElements.map((element) async { await Future.wait(signatureElements.map((element) async {
element.attributes['class'] = 'tmail-signature-blocked'; element.attributes['class'] = 'tmail-signature-blocked';
@@ -3,34 +3,33 @@ import 'package:core/presentation/utils/html_transformer/message_content_transfo
class HtmlTransform { class HtmlTransform {
final String _contentHtml; final DioClient _dioClient;
Map<String, String>? mapUrlDownloadCID;
DioClient? dioClient;
HtmlTransform( HtmlTransform(this._dioClient);
this._contentHtml,
{
this.mapUrlDownloadCID,
this.dioClient,
}
);
/// Transforms this message to HTML code. /// Transforms this message to HTML code.
Future<String> transformToHtml({TransformConfiguration? transformConfiguration}) async { Future<String> transformToHtml({
required String contentHtml,
Map<String, String>? mapUrlDownloadCID,
TransformConfiguration? transformConfiguration,
}) async {
transformConfiguration ??= TransformConfiguration.create(); transformConfiguration ??= TransformConfiguration.create();
final transformer = MessageContentTransformer(transformConfiguration); final transformer = MessageContentTransformer(transformConfiguration, _dioClient);
final document = await transformer.toDocument( final document = await transformer.toDocument(
_contentHtml, message: contentHtml,
mapUrlDownloadCID: mapUrlDownloadCID, mapUrlDownloadCID: mapUrlDownloadCID
dioClient: dioClient); );
return document.outerHtml; return document.outerHtml;
} }
/// Transforms this message to Text Plain. /// Transforms this message to Text Plain.
String transformToTextPlain({TransformConfiguration? transformConfiguration}) { String transformToTextPlain({
required String content,
TransformConfiguration? transformConfiguration
}) {
transformConfiguration ??= TransformConfiguration.create(); transformConfiguration ??= TransformConfiguration.create();
final transformer = MessageContentTransformer(transformConfiguration); final transformer = MessageContentTransformer(transformConfiguration, _dioClient);
final message = transformer.toMessage(_contentHtml); final message = transformer.toMessage(content);
return message; return message;
} }
} }
@@ -1,4 +1,5 @@
import 'package:core/core.dart'; import 'package:core/data/network/dio_client.dart';
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
import 'package:html/dom.dart'; import 'package:html/dom.dart';
import 'package:html/parser.dart' show parse; import 'package:html/parser.dart' show parse;
@@ -6,38 +7,35 @@ import 'package:html/parser.dart' show parse;
class MessageContentTransformer { class MessageContentTransformer {
/// The configuration used for the transformation /// The configuration used for the transformation
final TransformConfiguration configuration; final TransformConfiguration configuration;
final DioClient dioClient;
MessageContentTransformer(this.configuration); MessageContentTransformer(this.configuration, this.dioClient);
Future<void> _transformDocument( Future<void> _transformDocument({
Document document, required Document document,
{ Map<String, String>? mapUrlDownloadCID
Map<String, String>? mapUrlDownloadCID, }) async {
DioClient? dioClient
}
) async {
await Future.wait([ await Future.wait([
if (configuration.domTransformers.isNotEmpty) if (configuration.domTransformers.isNotEmpty)
...configuration.domTransformers.map((domTransformer) async => ...configuration.domTransformers.map((domTransformer) async =>
domTransformer.process( domTransformer.process(
document, document: document,
mapUrlDownloadCID: mapUrlDownloadCID, mapUrlDownloadCID: mapUrlDownloadCID,
dioClient: dioClient)) dioClient: dioClient
)
)
]); ]);
} }
Future<Document> toDocument( Future<Document> toDocument({
String message, required String message,
{ Map<String, String>? mapUrlDownloadCID
Map<String, String>? mapUrlDownloadCID, }) async {
DioClient? dioClient
}
) async {
final document = parse(message); final document = parse(message);
await _transformDocument( await _transformDocument(
document, document: document,
mapUrlDownloadCID: mapUrlDownloadCID, mapUrlDownloadCID: mapUrlDownloadCID,
dioClient: dioClient); );
return document; return document;
} }
@@ -13,10 +13,10 @@ import 'package:model/email/email_content_type.dart';
class HtmlAnalyzer { class HtmlAnalyzer {
final DioClient _dioClient; final HtmlTransform _htmlTransform;
final HtmlEscape _htmlEscape; final HtmlEscape _htmlEscape;
HtmlAnalyzer(this._dioClient, this._htmlEscape); HtmlAnalyzer(this._htmlTransform, this._htmlEscape);
Future<EmailContent> transformEmailContent( Future<EmailContent> transformEmailContent(
EmailContent emailContent, EmailContent emailContent,
@@ -25,13 +25,9 @@ class HtmlAnalyzer {
) async { ) async {
switch(emailContent.type) { switch(emailContent.type) {
case EmailContentType.textHtml: case EmailContentType.textHtml:
final htmlTransform = HtmlTransform( final htmlContent = await _htmlTransform.transformToHtml(
emailContent.content, contentHtml: emailContent.content,
dioClient: _dioClient, mapUrlDownloadCID: mapUrlDownloadCID,
mapUrlDownloadCID: mapUrlDownloadCID
);
final htmlContent = await htmlTransform.transformToHtml(
transformConfiguration: draftsEmail transformConfiguration: draftsEmail
? TransformConfiguration.create(customDomTransformers: TransformConfiguration.domTransformersForDraftEmail) ? TransformConfiguration.create(customDomTransformers: TransformConfiguration.domTransformersForDraftEmail)
: null : null
@@ -39,8 +35,8 @@ class HtmlAnalyzer {
return EmailContent(emailContent.type, htmlContent); return EmailContent(emailContent.type, htmlContent);
case EmailContentType.textPlain: case EmailContentType.textPlain:
final htmlTransform = HtmlTransform(emailContent.content); final message = _htmlTransform.transformToTextPlain(
final message = htmlTransform.transformToTextPlain( content: emailContent.content,
transformConfiguration: TransformConfiguration.create( transformConfiguration: TransformConfiguration.create(
customTextTransformers: [SanitizeAutolinkHtmlTransformers(_htmlEscape)] customTextTransformers: [SanitizeAutolinkHtmlTransformers(_htmlEscape)]
) )
@@ -54,10 +50,12 @@ class HtmlAnalyzer {
Future<EmailContent> addTooltipWhenHoverOnLink(EmailContent emailContent) async { Future<EmailContent> addTooltipWhenHoverOnLink(EmailContent emailContent) async {
switch(emailContent.type) { switch(emailContent.type) {
case EmailContentType.textHtml: case EmailContentType.textHtml:
final htmlTransform = HtmlTransform(emailContent.content); final htmlContent = await _htmlTransform.transformToHtml(
final htmlContent = await htmlTransform.transformToHtml( contentHtml: emailContent.content,
transformConfiguration: TransformConfiguration.create( transformConfiguration: TransformConfiguration.create(
customDomTransformers: [const AddTooltipLinkTransformer()])); customDomTransformers: [const AddTooltipLinkTransformer()]
)
);
return EmailContent(emailContent.type, htmlContent); return EmailContent(emailContent.type, htmlContent);
default: default:
return emailContent; return emailContent;
@@ -20,10 +20,15 @@ import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart';
class IdentityDataSourceImpl extends IdentityDataSource { class IdentityDataSourceImpl extends IdentityDataSource {
final HtmlTransform _htmlTransform;
final IdentityAPI _identityAPI; final IdentityAPI _identityAPI;
final ExceptionThrower _exceptionThrower; final ExceptionThrower _exceptionThrower;
IdentityDataSourceImpl(this._identityAPI, this._exceptionThrower); IdentityDataSourceImpl(
this._htmlTransform,
this._identityAPI,
this._exceptionThrower
);
@override @override
Future<IdentitiesResponse> getAllIdentities(Session session, AccountId accountId, {Properties? properties}) { Future<IdentitiesResponse> getAllIdentities(Session session, AccountId accountId, {Properties? properties}) {
@@ -56,7 +61,8 @@ class IdentityDataSourceImpl extends IdentityDataSource {
@override @override
Future<String> transformHtmlSignature(String signature) { Future<String> transformHtmlSignature(String signature) {
return Future.sync(() async { return Future.sync(() async {
final signatureUnescape = await HtmlTransform(signature).transformToHtml( final signatureUnescape = await _htmlTransform.transformToHtml(
contentHtml: signature,
transformConfiguration: TransformConfiguration.create(customDomTransformers: [ transformConfiguration: TransformConfiguration.create(customDomTransformers: [
const RemoveScriptTransformer(), const RemoveScriptTransformer(),
const BlockQuotedTransformer(), const BlockQuotedTransformer(),
@@ -1,3 +1,4 @@
import 'package:core/presentation/utils/html_transformer/html_transform.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:tmail_ui_user/features/base/interactors_bindings.dart'; import 'package:tmail_ui_user/features/base/interactors_bindings.dart';
import 'package:tmail_ui_user/features/manage_account/data/datasource/identity_data_source.dart'; import 'package:tmail_ui_user/features/manage_account/data/datasource/identity_data_source.dart';
@@ -35,6 +36,7 @@ class IdentityInteractorsBindings extends InteractorsBindings {
@override @override
void bindingsDataSourceImpl() { void bindingsDataSourceImpl() {
Get.lazyPut(() => IdentityDataSourceImpl( Get.lazyPut(() => IdentityDataSourceImpl(
Get.find<HtmlTransform>(),
Get.find<IdentityAPI>(), Get.find<IdentityAPI>(),
Get.find<RemoteExceptionThrower>())); Get.find<RemoteExceptionThrower>()));
} }
@@ -110,6 +110,7 @@ class NetworkBindings extends Bindings {
void _bindingTransformer() { void _bindingTransformer() {
Get.put(const HtmlEscape()); Get.put(const HtmlEscape());
Get.put(HtmlAnalyzer(Get.find<DioClient>(), Get.find<HtmlEscape>())); Get.put(HtmlTransform(Get.find<DioClient>()));
Get.put(HtmlAnalyzer(Get.find<HtmlTransform>(), Get.find<HtmlEscape>()));
} }
} }