feat: Cleanup Event.plaintextBody and add [plaintextBody] to Event.getLocalizedBody
It appears that [hideEdit] in Event.getLocalizedBody was written in a way that it assumes a valid event body. This was also fixed, while also adding tests for the various parameters of Event.getLocalizedBody
This commit is contained in:
parent
b849c828e3
commit
f3f9b219e1
|
|
@ -568,31 +568,52 @@ class Event extends MatrixEvent {
|
||||||
|
|
||||||
/// Returns a localized String representation of this event. For a
|
/// Returns a localized String representation of this event. For a
|
||||||
/// room list you may find [withSenderNamePrefix] useful. Set [hideReply] to
|
/// room list you may find [withSenderNamePrefix] useful. Set [hideReply] to
|
||||||
/// crop all lines starting with '>'.
|
/// crop all lines starting with '>'. With [plaintextBody] it'll use the
|
||||||
|
/// plaintextBody instead of the normal body.
|
||||||
String getLocalizedBody(
|
String getLocalizedBody(
|
||||||
MatrixLocalizations i18n, {
|
MatrixLocalizations i18n, {
|
||||||
bool withSenderNamePrefix = false,
|
bool withSenderNamePrefix = false,
|
||||||
bool hideReply = false,
|
bool hideReply = false,
|
||||||
bool hideEdit = false,
|
bool hideEdit = false,
|
||||||
|
bool plaintextBody = false,
|
||||||
}) {
|
}) {
|
||||||
if (redacted) {
|
if (redacted) {
|
||||||
return i18n.removedBy(redactedBecause.sender.calcDisplayname());
|
return i18n.removedBy(redactedBecause.sender.calcDisplayname());
|
||||||
}
|
}
|
||||||
|
var body = plaintextBody ? this.plaintextBody : this.body;
|
||||||
|
|
||||||
|
// we need to know if the message is an html message to be able to determine
|
||||||
|
// if we need to strip the reply fallback.
|
||||||
|
var htmlMessage = content['format'] != 'org.matrix.custom.html';
|
||||||
|
// If we have an edit, we want to operate on the new content
|
||||||
|
if (hideEdit &&
|
||||||
|
relationshipType == RelationshipTypes.edit &&
|
||||||
|
content.tryGet<Map<String, dynamic>>('m.new_content') != null) {
|
||||||
|
if (plaintextBody &&
|
||||||
|
content['m.new_content']['format'] == 'org.matrix.custom.html') {
|
||||||
|
htmlMessage = true;
|
||||||
|
body = HtmlToText.convert(
|
||||||
|
(content['m.new_content'] as Map<String, dynamic>)
|
||||||
|
.tryGet<String>('formatted_body') ??
|
||||||
|
formattedText);
|
||||||
|
} else {
|
||||||
|
htmlMessage = false;
|
||||||
|
body = (content['m.new_content'] as Map<String, dynamic>)
|
||||||
|
.tryGet<String>('body') ??
|
||||||
|
body;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Hide reply fallback
|
||||||
|
// Be sure that the plaintextBody already stripped teh reply fallback,
|
||||||
|
// if the message is formatted
|
||||||
|
if (hideReply && (!plaintextBody || htmlMessage)) {
|
||||||
|
body = body.replaceFirst(
|
||||||
|
RegExp(r'^>( \*)? <[^>]+>[^\n\r]+\r?\n(> [^\n]*\r?\n)*\r?\n'), '');
|
||||||
|
}
|
||||||
final callback = EventLocalizations.localizationsMap[type];
|
final callback = EventLocalizations.localizationsMap[type];
|
||||||
var localizedBody = i18n.unknownEvent(type);
|
var localizedBody = i18n.unknownEvent(type);
|
||||||
if (callback != null) {
|
if (callback != null) {
|
||||||
localizedBody = callback(this, i18n);
|
localizedBody = callback(this, i18n, body);
|
||||||
}
|
|
||||||
// Hide reply fallback
|
|
||||||
if (hideReply) {
|
|
||||||
localizedBody = localizedBody.replaceFirst(
|
|
||||||
RegExp(r'^>( \*)? <[^>]+>[^\n\r]+\r?\n(> [^\n]*\r?\n)*\r?\n'), '');
|
|
||||||
}
|
|
||||||
// Hide edit fallback
|
|
||||||
if (hideEdit &&
|
|
||||||
relationshipType == RelationshipTypes.edit &&
|
|
||||||
content.containsKey('m.new_content')) {
|
|
||||||
localizedBody = content['m.new_content']['body'] ?? localizedBody;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the sender name prefix
|
// Add the sender name prefix
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,13 @@ import '../room.dart';
|
||||||
import 'matrix_localizations.dart';
|
import 'matrix_localizations.dart';
|
||||||
|
|
||||||
abstract class EventLocalizations {
|
abstract class EventLocalizations {
|
||||||
|
// As we need to create the localized body off of a different set of parameters, we
|
||||||
|
// might create it with `event.plaintextBody`, maybe with `event.body`, maybe with the
|
||||||
|
// reply fallback stripped, and maybe with the new body in `event.content['m.new_content']`.
|
||||||
|
// Thus, it seems easier to offload that logic into `Event.getLocalizedBody()` and pass the
|
||||||
|
// `body` variable around here.
|
||||||
static String _localizedBodyNormalMessage(
|
static String _localizedBodyNormalMessage(
|
||||||
Event event, MatrixLocalizations i18n) {
|
Event event, MatrixLocalizations i18n, String body) {
|
||||||
switch (event.messageType) {
|
switch (event.messageType) {
|
||||||
case MessageTypes.Image:
|
case MessageTypes.Image:
|
||||||
return i18n.sentAPicture(event.sender.calcDisplayname());
|
return i18n.sentAPicture(event.sender.calcDisplayname());
|
||||||
|
|
@ -40,7 +45,7 @@ abstract class EventLocalizations {
|
||||||
case MessageTypes.Sticker:
|
case MessageTypes.Sticker:
|
||||||
return i18n.sentASticker(event.sender.calcDisplayname());
|
return i18n.sentASticker(event.sender.calcDisplayname());
|
||||||
case MessageTypes.Emote:
|
case MessageTypes.Emote:
|
||||||
return '* ${event.body}';
|
return '* $body';
|
||||||
case MessageTypes.BadEncrypted:
|
case MessageTypes.BadEncrypted:
|
||||||
String errorText;
|
String errorText;
|
||||||
switch (event.body) {
|
switch (event.body) {
|
||||||
|
|
@ -57,7 +62,7 @@ abstract class EventLocalizations {
|
||||||
errorText = i18n.noPermission + '.';
|
errorText = i18n.noPermission + '.';
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
errorText = event.body;
|
errorText = body;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return i18n.couldNotDecryptMessage(errorText);
|
return i18n.couldNotDecryptMessage(errorText);
|
||||||
|
|
@ -65,27 +70,27 @@ abstract class EventLocalizations {
|
||||||
case MessageTypes.Notice:
|
case MessageTypes.Notice:
|
||||||
case MessageTypes.None:
|
case MessageTypes.None:
|
||||||
default:
|
default:
|
||||||
return event.body;
|
return body;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This map holds how to localize event types, and thus which event types exist.
|
// This map holds how to localize event types, and thus which event types exist.
|
||||||
// If an event exists but it does not have a localized body, set its callback to null
|
// If an event exists but it does not have a localized body, set its callback to null
|
||||||
static final Map<String,
|
static final Map<String,
|
||||||
String Function(Event event, MatrixLocalizations i18n)>
|
String Function(Event event, MatrixLocalizations i18n, String body)>
|
||||||
localizationsMap = {
|
localizationsMap = {
|
||||||
EventTypes.Sticker: (event, i18n) =>
|
EventTypes.Sticker: (event, i18n, body) =>
|
||||||
i18n.sentASticker(event.sender.calcDisplayname()),
|
i18n.sentASticker(event.sender.calcDisplayname()),
|
||||||
EventTypes.Redaction: (event, i18n) =>
|
EventTypes.Redaction: (event, i18n, body) =>
|
||||||
i18n.redactedAnEvent(event.sender.calcDisplayname()),
|
i18n.redactedAnEvent(event.sender.calcDisplayname()),
|
||||||
EventTypes.RoomAliases: (event, i18n) =>
|
EventTypes.RoomAliases: (event, i18n, body) =>
|
||||||
i18n.changedTheRoomAliases(event.sender.calcDisplayname()),
|
i18n.changedTheRoomAliases(event.sender.calcDisplayname()),
|
||||||
EventTypes.RoomCanonicalAlias: (event, i18n) =>
|
EventTypes.RoomCanonicalAlias: (event, i18n, body) =>
|
||||||
i18n.changedTheRoomInvitationLink(event.sender.calcDisplayname()),
|
i18n.changedTheRoomInvitationLink(event.sender.calcDisplayname()),
|
||||||
EventTypes.RoomCreate: (event, i18n) =>
|
EventTypes.RoomCreate: (event, i18n, body) =>
|
||||||
i18n.createdTheChat(event.sender.calcDisplayname()),
|
i18n.createdTheChat(event.sender.calcDisplayname()),
|
||||||
EventTypes.RoomTombstone: (event, i18n) => i18n.roomHasBeenUpgraded,
|
EventTypes.RoomTombstone: (event, i18n, body) => i18n.roomHasBeenUpgraded,
|
||||||
EventTypes.RoomJoinRules: (event, i18n) {
|
EventTypes.RoomJoinRules: (event, i18n, body) {
|
||||||
final joinRules = JoinRules.values.firstWhere(
|
final joinRules = JoinRules.values.firstWhere(
|
||||||
(r) =>
|
(r) =>
|
||||||
r.toString().replaceAll('JoinRules.', '') ==
|
r.toString().replaceAll('JoinRules.', '') ==
|
||||||
|
|
@ -98,7 +103,7 @@ abstract class EventLocalizations {
|
||||||
event.sender.calcDisplayname(), joinRules.getLocalizedString(i18n));
|
event.sender.calcDisplayname(), joinRules.getLocalizedString(i18n));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
EventTypes.RoomMember: (event, i18n) {
|
EventTypes.RoomMember: (event, i18n, body) {
|
||||||
var text = 'Failed to parse member event';
|
var text = 'Failed to parse member event';
|
||||||
final targetName = event.stateKeyUser.calcDisplayname();
|
final targetName = event.stateKeyUser.calcDisplayname();
|
||||||
// Has the membership changed?
|
// Has the membership changed?
|
||||||
|
|
@ -162,15 +167,16 @@ abstract class EventLocalizations {
|
||||||
}
|
}
|
||||||
return text;
|
return text;
|
||||||
},
|
},
|
||||||
EventTypes.RoomPowerLevels: (event, i18n) =>
|
EventTypes.RoomPowerLevels: (event, i18n, body) =>
|
||||||
i18n.changedTheChatPermissions(event.sender.calcDisplayname()),
|
i18n.changedTheChatPermissions(event.sender.calcDisplayname()),
|
||||||
EventTypes.RoomName: (event, i18n) => i18n.changedTheChatNameTo(
|
EventTypes.RoomName: (event, i18n, body) => i18n.changedTheChatNameTo(
|
||||||
event.sender.calcDisplayname(), event.content['name']),
|
event.sender.calcDisplayname(), event.content['name']),
|
||||||
EventTypes.RoomTopic: (event, i18n) => i18n.changedTheChatDescriptionTo(
|
EventTypes.RoomTopic: (event, i18n, body) =>
|
||||||
|
i18n.changedTheChatDescriptionTo(
|
||||||
event.sender.calcDisplayname(), event.content['topic']),
|
event.sender.calcDisplayname(), event.content['topic']),
|
||||||
EventTypes.RoomAvatar: (event, i18n) =>
|
EventTypes.RoomAvatar: (event, i18n, body) =>
|
||||||
i18n.changedTheChatAvatar(event.sender.calcDisplayname()),
|
i18n.changedTheChatAvatar(event.sender.calcDisplayname()),
|
||||||
EventTypes.GuestAccess: (event, i18n) {
|
EventTypes.GuestAccess: (event, i18n, body) {
|
||||||
final guestAccess = GuestAccess.values.firstWhere(
|
final guestAccess = GuestAccess.values.firstWhere(
|
||||||
(r) =>
|
(r) =>
|
||||||
r.toString().replaceAll('GuestAccess.', '') ==
|
r.toString().replaceAll('GuestAccess.', '') ==
|
||||||
|
|
@ -183,7 +189,7 @@ abstract class EventLocalizations {
|
||||||
guestAccess.getLocalizedString(i18n));
|
guestAccess.getLocalizedString(i18n));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
EventTypes.HistoryVisibility: (event, i18n) {
|
EventTypes.HistoryVisibility: (event, i18n, body) {
|
||||||
final historyVisibility = HistoryVisibility.values.firstWhere(
|
final historyVisibility = HistoryVisibility.values.firstWhere(
|
||||||
(r) =>
|
(r) =>
|
||||||
r.toString().replaceAll('HistoryVisibility.', '') ==
|
r.toString().replaceAll('HistoryVisibility.', '') ==
|
||||||
|
|
@ -197,7 +203,7 @@ abstract class EventLocalizations {
|
||||||
historyVisibility.getLocalizedString(i18n));
|
historyVisibility.getLocalizedString(i18n));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
EventTypes.Encryption: (event, i18n) {
|
EventTypes.Encryption: (event, i18n, body) {
|
||||||
var localizedBody =
|
var localizedBody =
|
||||||
i18n.activatedEndToEndEncryption(event.sender.calcDisplayname());
|
i18n.activatedEndToEndEncryption(event.sender.calcDisplayname());
|
||||||
if (!event.room.client.encryptionEnabled) {
|
if (!event.room.client.encryptionEnabled) {
|
||||||
|
|
@ -205,18 +211,18 @@ abstract class EventLocalizations {
|
||||||
}
|
}
|
||||||
return localizedBody;
|
return localizedBody;
|
||||||
},
|
},
|
||||||
EventTypes.CallAnswer: (event, i18n) =>
|
EventTypes.CallAnswer: (event, i18n, body) =>
|
||||||
i18n.answeredTheCall(event.sender.calcDisplayname()),
|
i18n.answeredTheCall(event.sender.calcDisplayname()),
|
||||||
EventTypes.CallHangup: (event, i18n) =>
|
EventTypes.CallHangup: (event, i18n, body) =>
|
||||||
i18n.endedTheCall(event.sender.calcDisplayname()),
|
i18n.endedTheCall(event.sender.calcDisplayname()),
|
||||||
EventTypes.CallInvite: (event, i18n) =>
|
EventTypes.CallInvite: (event, i18n, body) =>
|
||||||
i18n.startedACall(event.sender.calcDisplayname()),
|
i18n.startedACall(event.sender.calcDisplayname()),
|
||||||
EventTypes.CallCandidates: (event, i18n) =>
|
EventTypes.CallCandidates: (event, i18n, body) =>
|
||||||
i18n.sentCallInformations(event.sender.calcDisplayname()),
|
i18n.sentCallInformations(event.sender.calcDisplayname()),
|
||||||
EventTypes.Encrypted: (event, i18n) =>
|
EventTypes.Encrypted: (event, i18n, body) =>
|
||||||
_localizedBodyNormalMessage(event, i18n),
|
_localizedBodyNormalMessage(event, i18n, body),
|
||||||
EventTypes.Message: (event, i18n) =>
|
EventTypes.Message: (event, i18n, body) =>
|
||||||
_localizedBodyNormalMessage(event, i18n),
|
_localizedBodyNormalMessage(event, i18n, body),
|
||||||
EventTypes.Reaction: null,
|
EventTypes.Reaction: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,19 @@ class HtmlToText {
|
||||||
/// Convert an HTML string to a pseudo-markdown plain text representation, with
|
/// Convert an HTML string to a pseudo-markdown plain text representation, with
|
||||||
/// `data-mx-spoiler` spans redacted
|
/// `data-mx-spoiler` spans redacted
|
||||||
static String convert(String html) {
|
static String convert(String html) {
|
||||||
|
// riot-web is notorious for creating bad reply fallback events from invalid messages which, if
|
||||||
|
// not handled properly, can lead to impersonation. As such, we strip the entire `<mx-reply>` tags
|
||||||
|
// here already, to prevent that from happening.
|
||||||
|
// We do *not* do this in an AST and just with simple regex here, as riot-web tends to create
|
||||||
|
// miss-matching tags, and this way we actually correctly identify what we want to strip and, well,
|
||||||
|
// strip it.
|
||||||
|
final renderHtml = html.replaceAll(
|
||||||
|
RegExp('<mx-reply>.*<\/mx-reply>',
|
||||||
|
caseSensitive: false, multiLine: false, dotAll: true),
|
||||||
|
'');
|
||||||
|
|
||||||
final opts = _ConvertOpts();
|
final opts = _ConvertOpts();
|
||||||
var reply = _walkNode(opts, parseFragment(html));
|
var reply = _walkNode(opts, parseFragment(renderHtml));
|
||||||
reply = reply.replaceAll(RegExp(r'\s*$', multiLine: false), '');
|
reply = reply.replaceAll(RegExp(r'\s*$', multiLine: false), '');
|
||||||
return reply;
|
return reply;
|
||||||
}
|
}
|
||||||
|
|
@ -105,19 +116,19 @@ class HtmlToText {
|
||||||
opts.listDepth++;
|
opts.listDepth++;
|
||||||
final entries = _listChildNodes(opts, node, {'li'});
|
final entries = _listChildNodes(opts, node, {'li'});
|
||||||
opts.listDepth--;
|
opts.listDepth--;
|
||||||
var entry = 0;
|
var entry = 1;
|
||||||
if (node.attributes['start'] is String &&
|
if (node.attributes['start'] is String &&
|
||||||
RegExp(r'^[0-9]+$', multiLine: false)
|
RegExp(r'^[0-9]+$', multiLine: false)
|
||||||
.hasMatch(node.attributes['start'])) {
|
.hasMatch(node.attributes['start'])) {
|
||||||
entry = int.parse(node.attributes['start']);
|
entry = int.parse(node.attributes['start']);
|
||||||
}
|
}
|
||||||
|
|
||||||
return entries.map((s) {
|
return entries
|
||||||
entry++;
|
.map((s) =>
|
||||||
return (' ' * opts.listDepth) +
|
(' ' * opts.listDepth) +
|
||||||
'$entry. ' +
|
'${entry++}. ' +
|
||||||
s.replaceAll('\n', '\n' + (' ' * opts.listDepth) + ' ');
|
s.replaceAll('\n', '\n' + (' ' * opts.listDepth) + ' '))
|
||||||
}).join('\n');
|
.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
static const _listBulletPoints = <String>['●', '○', '■', '‣'];
|
static const _listBulletPoints = <String>['●', '○', '■', '‣'];
|
||||||
|
|
|
||||||
|
|
@ -895,6 +895,81 @@ void main() {
|
||||||
expect(event.isEventTypeKnown, false);
|
expect(event.isEventTypeKnown, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('getLocalizedBody, parameters', () {
|
||||||
|
final matrix = Client('testclient', httpClient: FakeMatrixApi());
|
||||||
|
final room = Room(id: '!1234:example.com', client: matrix);
|
||||||
|
var event = Event.fromJson({
|
||||||
|
'content': {
|
||||||
|
'body': 'This is an example text message',
|
||||||
|
'format': 'org.matrix.custom.html',
|
||||||
|
'formatted_body': '<b>This is an example text message</b>',
|
||||||
|
'msgtype': 'm.text'
|
||||||
|
},
|
||||||
|
'event_id': '\$143273582443PhrSn:example.org',
|
||||||
|
'origin_server_ts': 1432735824653,
|
||||||
|
'room_id': '!jEsUZKDJdhlrceRyVU:example.org',
|
||||||
|
'sender': '@example:example.org',
|
||||||
|
'type': 'm.room.message',
|
||||||
|
'unsigned': {'age': 1234}
|
||||||
|
}, room);
|
||||||
|
expect(
|
||||||
|
event.getLocalizedBody(FakeMatrixLocalizations(),
|
||||||
|
plaintextBody: true),
|
||||||
|
'**This is an example text message**');
|
||||||
|
|
||||||
|
event = Event.fromJson({
|
||||||
|
'content': {
|
||||||
|
'body': '* This is an example text message',
|
||||||
|
'format': 'org.matrix.custom.html',
|
||||||
|
'formatted_body': '* <b>This is an example text message</b>',
|
||||||
|
'msgtype': 'm.text',
|
||||||
|
'm.relates_to': <String, dynamic>{
|
||||||
|
'rel_type': 'm.replace',
|
||||||
|
'event_id': '\$some_event',
|
||||||
|
},
|
||||||
|
'm.new_content': <String, dynamic>{
|
||||||
|
'body': 'This is an example text message',
|
||||||
|
'format': 'org.matrix.custom.html',
|
||||||
|
'formatted_body': '<b>This is an example text message</b>',
|
||||||
|
'msgtype': 'm.text'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'event_id': '\$143273582443PhrSn:example.org',
|
||||||
|
'origin_server_ts': 1432735824653,
|
||||||
|
'room_id': '!jEsUZKDJdhlrceRyVU:example.org',
|
||||||
|
'sender': '@example:example.org',
|
||||||
|
'type': 'm.room.message',
|
||||||
|
'unsigned': {'age': 1234}
|
||||||
|
}, room);
|
||||||
|
expect(event.getLocalizedBody(FakeMatrixLocalizations(), hideEdit: true),
|
||||||
|
'This is an example text message');
|
||||||
|
expect(
|
||||||
|
event.getLocalizedBody(FakeMatrixLocalizations(),
|
||||||
|
hideEdit: true, plaintextBody: true),
|
||||||
|
'**This is an example text message**');
|
||||||
|
|
||||||
|
event = Event.fromJson({
|
||||||
|
'content': {
|
||||||
|
'body': '> <@user:example.org> beep\n\nhmm, fox',
|
||||||
|
'format': 'org.matrix.custom.html',
|
||||||
|
'formatted_body': '<mx-reply>beep</mx-reply>hmm, <em>fox</em>',
|
||||||
|
'msgtype': 'm.text'
|
||||||
|
},
|
||||||
|
'event_id': '\$143273582443PhrSn:example.org',
|
||||||
|
'origin_server_ts': 1432735824653,
|
||||||
|
'room_id': '!jEsUZKDJdhlrceRyVU:example.org',
|
||||||
|
'sender': '@example:example.org',
|
||||||
|
'type': 'm.room.message',
|
||||||
|
'unsigned': {'age': 1234}
|
||||||
|
}, room);
|
||||||
|
expect(event.getLocalizedBody(FakeMatrixLocalizations(), hideReply: true),
|
||||||
|
'hmm, fox');
|
||||||
|
expect(
|
||||||
|
event.getLocalizedBody(FakeMatrixLocalizations(),
|
||||||
|
hideReply: true, plaintextBody: true),
|
||||||
|
'hmm, *fox*');
|
||||||
|
});
|
||||||
|
|
||||||
test('aggregations', () {
|
test('aggregations', () {
|
||||||
final event = Event.fromJson({
|
final event = Event.fromJson({
|
||||||
'content': {
|
'content': {
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,7 @@ void main() {
|
||||||
'<ul><li>hey<ul><li>a</li><li>b</li></ul></li><li>foxies</li></ul>':
|
'<ul><li>hey<ul><li>a</li><li>b</li></ul></li><li>foxies</li></ul>':
|
||||||
'● hey\n ○ a\n ○ b\n● foxies',
|
'● hey\n ○ a\n ○ b\n● foxies',
|
||||||
'<ol><li>a</li><li>b</li></ol>': '1. a\n2. b',
|
'<ol><li>a</li><li>b</li></ol>': '1. a\n2. b',
|
||||||
|
'<ol start="42"><li>a</li><li>b</li></ol>': '42. a\n43. b',
|
||||||
'<ol><li>a<ol><li>aa</li><li>bb</li></ol></li><li>b</li></ol>':
|
'<ol><li>a<ol><li>aa</li><li>bb</li></ol></li><li>b</li></ol>':
|
||||||
'1. a\n 1. aa\n 2. bb\n2. b',
|
'1. a\n 1. aa\n 2. bb\n2. b',
|
||||||
'<ol><li>a<ul><li>aa</li><li>bb</li></ul></li><li>b</li></ol>':
|
'<ol><li>a<ul><li>aa</li><li>bb</li></ul></li><li>b</li></ol>':
|
||||||
|
|
@ -90,6 +91,7 @@ void main() {
|
||||||
'<h6>fox</h6>': '###### fox',
|
'<h6>fox</h6>': '###### fox',
|
||||||
'<span>fox</span>': 'fox',
|
'<span>fox</span>': 'fox',
|
||||||
'<p>fox</p>\n<p>floof</p>': 'fox\n\nfloof',
|
'<p>fox</p>\n<p>floof</p>': 'fox\n\nfloof',
|
||||||
|
'<mx-reply>beep</mx-reply><p>fox</p>\n<p>floof</p>': 'fox\n\nfloof',
|
||||||
};
|
};
|
||||||
for (final entry in testMap.entries) {
|
for (final entry in testMap.entries) {
|
||||||
expect(HtmlToText.convert(entry.key), entry.value);
|
expect(HtmlToText.convert(entry.key), entry.value);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue