TF-105 Create html transformer
This commit is contained in:
@@ -0,0 +1,21 @@
|
|||||||
|
|
||||||
|
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
|
||||||
|
import 'package:html/dom.dart';
|
||||||
|
|
||||||
|
/// Transforms the HTML DOM.
|
||||||
|
abstract class DomTransformer {
|
||||||
|
|
||||||
|
const DomTransformer();
|
||||||
|
|
||||||
|
/// Uses the `DOM` [document] and specified [message] to transform the `document`.
|
||||||
|
///
|
||||||
|
/// All changes will be visible to subsequent transformers.
|
||||||
|
void process(Document document, String message, TransformConfiguration configuration);
|
||||||
|
|
||||||
|
/// Adds a HEAD element if necessary
|
||||||
|
void ensureDocumentHeadIsAvailable(Document document) {
|
||||||
|
if (document.head == null) {
|
||||||
|
document.children.insert(0, Element.html('<head></head>'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
|
||||||
|
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
|
||||||
|
|
||||||
|
/// Transforms plain text messages.
|
||||||
|
abstract class TextTransformer {
|
||||||
|
const TextTransformer();
|
||||||
|
|
||||||
|
String transform(String text, String message, TransformConfiguration configuration);
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
|
||||||
|
import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart';
|
||||||
|
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
|
||||||
|
import 'package:html/dom.dart';
|
||||||
|
|
||||||
|
class SetStyleBlockQuoteTransformer extends DomTransformer {
|
||||||
|
|
||||||
|
const SetStyleBlockQuoteTransformer();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void process(Document document, String message, TransformConfiguration configuration) {
|
||||||
|
final quoteElements = document.getElementsByTagName('blockquote');
|
||||||
|
for (final quote in quoteElements) {
|
||||||
|
final style = quote.attributes['style'];
|
||||||
|
if (style == null) {
|
||||||
|
quote.attributes['style'] = '''
|
||||||
|
margin: 0px 8px 0px 8px;
|
||||||
|
padding: 8px 16px 8px 16px;
|
||||||
|
border-left:5px solid #eee;
|
||||||
|
''';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
|
||||||
|
import 'package:html/dom.dart';
|
||||||
|
import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart';
|
||||||
|
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
|
||||||
|
|
||||||
|
class ImageTransformer extends DomTransformer {
|
||||||
|
|
||||||
|
const ImageTransformer();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void process(Document document, String message, TransformConfiguration configuration) {
|
||||||
|
final imageElements = document.getElementsByTagName('img');
|
||||||
|
for (final imageElement in imageElements) {
|
||||||
|
final src = imageElement.attributes['src'];
|
||||||
|
if (src != null) {
|
||||||
|
if (src.startsWith('http')) {
|
||||||
|
if (configuration.blockExternalImages) {
|
||||||
|
imageElement.attributes.remove('src');
|
||||||
|
} else if (src.startsWith('http:')) {
|
||||||
|
// always at least enforce HTTPS images:
|
||||||
|
final url = src.substring('http:'.length);
|
||||||
|
imageElement.attributes['src'] = 'https:$url';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final style = imageElement.attributes['style'];
|
||||||
|
if (style == null) {
|
||||||
|
imageElement.attributes['style'] = 'display: inline;max-width: 100%;height: auto;';
|
||||||
|
} else {
|
||||||
|
imageElement.attributes['style'] = 'display: inline;max-width: 100%;height: auto;' + style;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart';
|
||||||
|
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
|
||||||
|
import 'package:html/dom.dart';
|
||||||
|
|
||||||
|
class EnsureRelationNoReferrerTransformer extends DomTransformer {
|
||||||
|
|
||||||
|
const EnsureRelationNoReferrerTransformer();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void process(Document document, String message, TransformConfiguration configuration) {
|
||||||
|
final linkElements = document.getElementsByTagName('a');
|
||||||
|
for (final linkElement in linkElements) {
|
||||||
|
linkElement.attributes['rel'] = 'noopener noreferrer';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
|
||||||
|
import 'package:html/dom.dart';
|
||||||
|
import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart';
|
||||||
|
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
|
||||||
|
|
||||||
|
class ViewPortTransformer extends DomTransformer {
|
||||||
|
|
||||||
|
static final Element _viewPortMetaElement = Element.html(
|
||||||
|
'<meta name="viewport" content="width=device-width, initial-scale=1.0">');
|
||||||
|
static final Element _contentTypeMetaElement = Element.html(
|
||||||
|
'<meta http-equiv="Content-Type" content="text/html; charset=utf-8">');
|
||||||
|
|
||||||
|
const ViewPortTransformer();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void process(Document document, String message, TransformConfiguration configuration) {
|
||||||
|
final metaElements = document.getElementsByTagName('meta');
|
||||||
|
var viewportNeedsToBeAdded = true;
|
||||||
|
var contentTypeNeedsToBeAdded = true;
|
||||||
|
for (final metaElement in metaElements) {
|
||||||
|
if (metaElement.attributes['name'] == 'viewport') {
|
||||||
|
viewportNeedsToBeAdded = false;
|
||||||
|
metaElement.attributes['content'] = 'width=device-width, initial-scale=1.0';
|
||||||
|
} else if (metaElement.attributes['charset'] != null) {
|
||||||
|
metaElement.attributes['charset'] = 'utf-8';
|
||||||
|
} else {
|
||||||
|
final httpEquiv = metaElement.attributes['http-equiv'];
|
||||||
|
if (httpEquiv != null && httpEquiv.toLowerCase() == 'content-type') {
|
||||||
|
contentTypeNeedsToBeAdded = false;
|
||||||
|
metaElement.attributes['content'] = 'text/html; charset=utf-8';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (contentTypeNeedsToBeAdded) {
|
||||||
|
ensureDocumentHeadIsAvailable(document);
|
||||||
|
document.head!.append(_contentTypeMetaElement);
|
||||||
|
}
|
||||||
|
if (viewportNeedsToBeAdded) {
|
||||||
|
ensureDocumentHeadIsAvailable(document);
|
||||||
|
document.head!.append(_viewPortMetaElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
import 'package:html/dom.dart';
|
||||||
|
import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart';
|
||||||
|
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
|
||||||
|
|
||||||
|
class RemoveScriptTransformer extends DomTransformer {
|
||||||
|
|
||||||
|
const RemoveScriptTransformer();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void process(Document document, String message, TransformConfiguration configuration) {
|
||||||
|
final scriptElements = document.getElementsByTagName('script');
|
||||||
|
for (final scriptElement in scriptElements) {
|
||||||
|
scriptElement.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
|
||||||
|
import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart';
|
||||||
|
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
|
||||||
|
import 'package:html/dom.dart';
|
||||||
|
|
||||||
|
class SetStyleTableTransformer extends DomTransformer {
|
||||||
|
|
||||||
|
const SetStyleTableTransformer();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void process(Document document, String message, TransformConfiguration configuration) {
|
||||||
|
final tableElements = document.getElementsByTagName('table');
|
||||||
|
for (final table in tableElements) {
|
||||||
|
final style = table.attributes['style'];
|
||||||
|
if (style == null) {
|
||||||
|
table.attributes['style'] = 'width: 100%;max-width: 100%;border:1px solid #f0f0f0;border-collapse: collapse;border-spacing: 2px;';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final tdElements = document.getElementsByTagName('td');
|
||||||
|
for (final tdTag in tdElements) {
|
||||||
|
final style = tdTag.attributes['style'];
|
||||||
|
if (style == null) {
|
||||||
|
tdTag.attributes['style'] = 'padding: 13px;margin: 0px;border:1px solid #f0f0f0;';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final thElements = document.getElementsByTagName('th');
|
||||||
|
for (final thTag in thElements) {
|
||||||
|
final style = thTag.attributes['style'];
|
||||||
|
if (style == null) {
|
||||||
|
thTag.attributes['style'] = 'padding: 13px;margin: 0px;border:1px solid #f0f0f0;';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import 'package:core/presentation/utils/html_transformer/message_content_transformer.dart';
|
||||||
|
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
|
||||||
|
import 'package:html/dom.dart';
|
||||||
|
|
||||||
|
class HtmlTransform {
|
||||||
|
|
||||||
|
final String _message;
|
||||||
|
|
||||||
|
HtmlTransform(this._message);
|
||||||
|
|
||||||
|
/// Transforms this message to HTML code.
|
||||||
|
///
|
||||||
|
/// Set [blockExternalImages] to `true` in case external images should be blocked.
|
||||||
|
/// Optionally specify the [transformConfiguration] to control all aspects of the transformation - in that case other parameters are ignored.
|
||||||
|
String transformToHtml({
|
||||||
|
bool? blockExternalImages,
|
||||||
|
TransformConfiguration? transformConfiguration,
|
||||||
|
}) {
|
||||||
|
final document = transformToDocument(
|
||||||
|
blockExternalImages: blockExternalImages,
|
||||||
|
transformConfiguration: transformConfiguration,
|
||||||
|
);
|
||||||
|
return document.outerHtml;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transforms this message to Document.
|
||||||
|
///
|
||||||
|
/// Set [blockExternalImages] to `true` in case external images should be blocked.
|
||||||
|
/// Optionally specify the [transformConfiguration] to control all aspects of the transformation - in that case other parameters are ignored.
|
||||||
|
Document transformToDocument({
|
||||||
|
bool? blockExternalImages,
|
||||||
|
int? maxImageWidth,
|
||||||
|
TransformConfiguration? transformConfiguration,
|
||||||
|
}) {
|
||||||
|
transformConfiguration ??= TransformConfiguration.create(
|
||||||
|
blockExternalImages: blockExternalImages,
|
||||||
|
maxImageWidth: maxImageWidth,
|
||||||
|
);
|
||||||
|
final transformer = MessageContentTransformer(transformConfiguration);
|
||||||
|
return transformer.toDocument(_message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
|
||||||
|
import 'package:html/dom.dart';
|
||||||
|
import 'package:html/parser.dart' show parse;
|
||||||
|
|
||||||
|
/// Transforms messages
|
||||||
|
class MessageContentTransformer {
|
||||||
|
/// The configuration used for the transformation
|
||||||
|
final TransformConfiguration configuration;
|
||||||
|
|
||||||
|
MessageContentTransformer(this.configuration);
|
||||||
|
|
||||||
|
void transformDocument(Document document, String message) {
|
||||||
|
for (final domTransformer in configuration.domTransformers) {
|
||||||
|
domTransformer.process(document, message, configuration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Document toDocument(String message) {
|
||||||
|
var html = message;
|
||||||
|
final document = parse(html);
|
||||||
|
transformDocument(document, message);
|
||||||
|
return document;
|
||||||
|
}
|
||||||
|
|
||||||
|
String toHtml(String message) {
|
||||||
|
return toDocument(message).outerHtml;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
|
||||||
|
import 'package:core/presentation/utils/html_transformer/base/text_transformer.dart';
|
||||||
|
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
|
||||||
|
|
||||||
|
class ConvertTagsTextTransformer implements TextTransformer {
|
||||||
|
|
||||||
|
const ConvertTagsTextTransformer();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String transform(String text, String message, TransformConfiguration configuration) {
|
||||||
|
return text.replaceAll('<', '<').replaceAll('>', '>');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
|
||||||
|
import 'package:core/presentation/utils/html_transformer/base/text_transformer.dart';
|
||||||
|
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
|
||||||
|
|
||||||
|
class LineBreakTextTransformer extends TextTransformer {
|
||||||
|
|
||||||
|
const LineBreakTextTransformer();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String transform(String text, String message, TransformConfiguration configuration) {
|
||||||
|
text = text.replaceAll('\r\n', '<br/>');
|
||||||
|
text = text.replaceAll('\n', '<br/>');
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
|
||||||
|
import 'package:core/presentation/utils/html_transformer/base/text_transformer.dart';
|
||||||
|
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
|
||||||
|
|
||||||
|
class LinksTextTransformer extends TextTransformer {
|
||||||
|
|
||||||
|
static final RegExp schemeRegEx = RegExp(r'[a-z]{3,6}://');
|
||||||
|
// Not a perfect but good enough regular expression to match URLs in text.
|
||||||
|
// It also matches a space at the beginning and a dot at the end,
|
||||||
|
// so this is filtered out manually in the found matches
|
||||||
|
static final RegExp linkRegEx = RegExp(
|
||||||
|
r'(([a-z]{3,6}:\/\/)|(^|\s))([a-zA-Z0-9\-]+\.)+[a-z]{2,13}([\?\/]+[\.\?\=\&\%\/\w\+\-]*)?');
|
||||||
|
const LinksTextTransformer();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String transform(String text, String message, TransformConfiguration configuration) {
|
||||||
|
final matches = linkRegEx.allMatches(text);
|
||||||
|
if (matches.isEmpty) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
final buffer = StringBuffer();
|
||||||
|
var end = 0;
|
||||||
|
for (final match in matches) {
|
||||||
|
if (match.end < text.length && text[match.end] == '@') {
|
||||||
|
// this is an email address, abort abort!
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final originalGroup = match.group(0)!;
|
||||||
|
final group = originalGroup.trimLeft();
|
||||||
|
final start = match.start + originalGroup.length - group.length;
|
||||||
|
buffer.write(text.substring(end, start));
|
||||||
|
final endsWithDot = group.endsWith('.');
|
||||||
|
final urlText =
|
||||||
|
endsWithDot ? group.substring(0, group.length - 1) : group;
|
||||||
|
buffer.write('<a href="');
|
||||||
|
if (!group.startsWith(schemeRegEx)) {
|
||||||
|
buffer.write('https://');
|
||||||
|
}
|
||||||
|
buffer..write(urlText)..write('">')..write(urlText)..write('</a>');
|
||||||
|
end = endsWithDot ? match.end - 1 : match.end;
|
||||||
|
}
|
||||||
|
if (end < text.length) {
|
||||||
|
buffer.write(text.substring(end));
|
||||||
|
}
|
||||||
|
return buffer.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
|
||||||
|
import 'package:core/presentation/utils/html_transformer/dom/blockquote_transformers.dart';
|
||||||
|
import 'package:core/presentation/utils/html_transformer/dom/image_transformers.dart';
|
||||||
|
import 'package:core/presentation/utils/html_transformer/dom/link_transformers.dart';
|
||||||
|
import 'package:core/presentation/utils/html_transformer/dom/meta_transformers.dart';
|
||||||
|
import 'package:core/presentation/utils/html_transformer/dom/script_transformers.dart';
|
||||||
|
import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart';
|
||||||
|
import 'package:core/presentation/utils/html_transformer/dom/table_transformers.dart';
|
||||||
|
import 'package:core/presentation/utils/html_transformer/text/convert_tags_text_transformer.dart';
|
||||||
|
import 'package:core/presentation/utils/html_transformer/text/linebreak_text_transformer.dart';
|
||||||
|
import 'package:core/presentation/utils/html_transformer/text/links_text_transformer.dart';
|
||||||
|
import 'package:core/presentation/utils/html_transformer/base/text_transformer.dart';
|
||||||
|
|
||||||
|
/// Contains the configuration for all transformations.
|
||||||
|
class TransformConfiguration {
|
||||||
|
|
||||||
|
/// Should external images be blocked?
|
||||||
|
final bool blockExternalImages;
|
||||||
|
|
||||||
|
/// The maximum width for embedded images. It make sense to limit this to reduce the generated HTML size.
|
||||||
|
final int? maxImageWidth;
|
||||||
|
|
||||||
|
/// The list of DOM transformers being used
|
||||||
|
final List<DomTransformer> domTransformers;
|
||||||
|
|
||||||
|
/// The list of text transformers that are used before a plain text message without HTML part is converted into HTML
|
||||||
|
final List<TextTransformer> textTransformers;
|
||||||
|
|
||||||
|
/// Creates a new transform configuration
|
||||||
|
///
|
||||||
|
/// Compare [create] to have an easier to use building function
|
||||||
|
const TransformConfiguration(
|
||||||
|
this.blockExternalImages,
|
||||||
|
this.maxImageWidth,
|
||||||
|
this.domTransformers,
|
||||||
|
this.textTransformers
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Provides easy access to a standard configuration that does not block external images.
|
||||||
|
static const TransformConfiguration standardConfiguration = TransformConfiguration(
|
||||||
|
false,
|
||||||
|
standardMaxImageWidth,
|
||||||
|
standardDomTransformers,
|
||||||
|
standardTextTransformers
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Provides an easy option to customize a configuration.
|
||||||
|
///
|
||||||
|
/// Any specified [customDomTransformers] or [customTextTransformers] are being appended to the standard transformers.
|
||||||
|
static TransformConfiguration create({
|
||||||
|
bool? blockExternalImages,
|
||||||
|
int? maxImageWidth,
|
||||||
|
List<DomTransformer>? customDomTransformers,
|
||||||
|
List<TextTransformer>? customTextTransformers
|
||||||
|
}) {
|
||||||
|
final domTransformers = (customDomTransformers != null)
|
||||||
|
? [...standardDomTransformers, ...customDomTransformers]
|
||||||
|
: [...standardDomTransformers];
|
||||||
|
final textTransformers = (customTextTransformers != null)
|
||||||
|
? [...standardTextTransformers, ...customTextTransformers]
|
||||||
|
: standardTextTransformers;
|
||||||
|
maxImageWidth ??= standardMaxImageWidth;
|
||||||
|
return TransformConfiguration(
|
||||||
|
blockExternalImages ?? false,
|
||||||
|
maxImageWidth,
|
||||||
|
domTransformers,
|
||||||
|
textTransformers
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const int? standardMaxImageWidth = null;
|
||||||
|
|
||||||
|
static const List<DomTransformer> standardDomTransformers = [
|
||||||
|
ViewPortTransformer(),
|
||||||
|
RemoveScriptTransformer(),
|
||||||
|
ImageTransformer(),
|
||||||
|
EnsureRelationNoReferrerTransformer(),
|
||||||
|
SetStyleBlockQuoteTransformer(),
|
||||||
|
SetStyleTableTransformer(),
|
||||||
|
];
|
||||||
|
|
||||||
|
static const List<TextTransformer> standardTextTransformers = [
|
||||||
|
ConvertTagsTextTransformer(),
|
||||||
|
LinksTextTransformer(),
|
||||||
|
LineBreakTextTransformer(),
|
||||||
|
];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user