Initial implementation of HTML Content to Editor conversion

codemagic-setup
Hank Grabowski 2023-03-18 14:49:20 -04:00
rodzic 328daa30a1
commit 4dbf21c656
3 zmienionych plików z 241 dodań i 1 usunięć

Wyświetl plik

@ -20,6 +20,7 @@ import '../models/timeline_entry.dart';
import '../services/feature_version_checker.dart';
import '../services/timeline_manager.dart';
import '../utils/active_profile_selector.dart';
import '../utils/html_to_edit_text_helper.dart';
import '../utils/snackbar_builder.dart';
class EditorScreen extends StatefulWidget {
@ -85,7 +86,7 @@ class _EditorScreenState extends State<EditorScreen> {
.andThenAsync((manager) async => await manager.getEntryById(widget.id));
result.match(onSuccess: (entry) {
_logger.fine('Loading status ${widget.id} information into fields');
contentController.text = entry.body;
contentController.text = toEditTextField(entry.body);
spoilerController.text = entry.spoilerText;
existingMediaItems
.addAll(entry.mediaAttachments.map((e) => e.toImageEntry()));

Wyświetl plik

@ -0,0 +1,115 @@
import 'package:html/dom.dart';
import 'package:html/parser.dart';
String toEditTextField(String htmlContentFragment) {
final dom = parseFragment(htmlContentFragment);
final segments = dom.nodes
.map((n) => n is Element ? n.elementToEditText() : n.nodeToEditText())
.toList();
return segments.join('');
}
extension NodeTextConverter on Node {
String nodeToEditText() {
if (nodes.isEmpty) {
final stringWithQuotes = toString();
final start = stringWithQuotes.startsWith('"') ? 1 : 0;
final end = stringWithQuotes.endsWith('"')
? stringWithQuotes.length - 1
: stringWithQuotes.length;
return stringWithQuotes.substring(start, end);
}
final convertedNodes = nodes
.map((n) => n is Element ? n.elementToEditText() : n.nodeToEditText())
.toList();
return convertedNodes.join('');
}
}
extension ElementTextConverter on Element {
String elementToEditText({int depth = 0}) {
late final String innerText;
late final String startText;
late final String endText;
switch (localName) {
case 'a':
startText = '';
innerText = htmlLinkToString();
endText = '';
break;
case 'br':
startText = '';
innerText = '';
endText = '\n';
break;
case 'p':
startText = '';
innerText = buildInnerText(depth);
endText = '\n';
break;
case 'em':
startText = '*';
innerText = buildInnerText(depth);
endText = '*';
break;
case 'strong':
startText = '**';
innerText = buildInnerText(depth);
endText = '**';
break;
case 'li':
startText = '\n${buildTabs(depth)}- ';
innerText = buildInnerText(depth);
endText = '';
break;
case 'ul':
startText = '';
innerText = buildInnerText(depth + 1);
endText = '';
break;
default:
startText = '<$localName>';
innerText = buildInnerText(depth);
endText = '</$localName>';
}
return '$startText$innerText$endText';
}
String htmlLinkToString() {
final attrs = attributes['class'] ?? '';
if (attrs.contains('hashtag')) {
return text;
}
if (attrs.contains('mention')) {
final uri = Uri.parse(attributes['href'] ?? '');
final host = uri.host;
final username = text;
return '$username@$host';
}
return attributes['href'] ?? 'No link found';
}
String buildInnerText(int depth) {
if (nodes.isEmpty) {
return '';
}
final convertedNodes = nodes
.map((n) => n is Element
? n.elementToEditText(depth: depth)
: n.nodeToEditText())
.toList();
return convertedNodes.join('');
}
String buildTabs(int depth) => depth == 0
? ''
: List.generate(
depth,
(index) => ' ',
).join('');
}

Wyświetl plik

@ -0,0 +1,124 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:relatica/utils/html_to_edit_text_helper.dart';
void testConversion(String original, String expectedOutput) {
final output = toEditTextField(original);
if (output != expectedOutput) {
print(output);
}
expect(output, equals(expectedOutput));
}
void main() {
test('Empty conversion', () {
const original = '';
const expected = '';
testConversion(original, expected);
});
test('Plain text no p-tags', () {
const original = 'This post is just text';
const expected = 'This post is just text';
testConversion(original, expected);
});
test('Plain text with p-tags', () {
const original = '<p>This post is just text</p>';
const expected = 'This post is just text\n';
testConversion(original, expected);
});
test('Formatting tags', () {
const original =
'<p>Post with <em>italics</em> <strong>bold</strong> <u>underlined</u></p>';
const expected = 'Post with *italics* **bold** <u>underlined</u>\n';
testConversion(original, expected);
});
test('Embedded link', () {
const original =
"Add preview again<br><a href=\"https://sdtimes.com/software-development/eclipse-foundation-finds-significant-momentum-for-open-source-java-this-year/\" target=\"_blank\" rel=\"noopener noreferrer\">sdtimes.com/software-developme…</a>";
const expected = '''
Add preview again
https://sdtimes.com/software-development/eclipse-foundation-finds-significant-momentum-for-open-source-java-this-year/''';
testConversion(original, expected);
});
test('Hashtags and mentions', () {
const original =
"Post with hashtags <a class=\"mention hashtag status-link\" href=\"https://friendicadevtest1.myportal.social/search?tag=linux\" rel=\"tag\">#<span>linux</span></a> and mentions <a class=\"u-url mention status-link\" href=\"https://friendicadevtest1.myportal.social/profile/testuser2\" rel=\"noopener noreferrer\" target=\"_blank\" title=\"testuser2\">@<span>testuser2</span></a>";
const expected =
'Post with hashtags #linux and mentions @testuser2@friendicadevtest1.myportal.social';
testConversion(original, expected);
});
test('Hashtags within p-tags', () {
const original =
"<p>Indie requests boops. </p><p><a href=\"https://scicomm.xyz/tags/AcademicDogs\" class=\"mention hashtag\" rel=\"nofollow noopener noreferrer\" target=\"_blank\">#<span>AcademicDogs</span></a></p>";
const expected = '''
Indie requests boops.
#AcademicDogs
''';
testConversion(original, expected);
});
test('Hashtags, links, breaks, and p-tags with unicode', () {
const original =
"<p>North Dakota 🏴 COVID-19 current stats for Sat Mar 18 2023</p><p>Cases: 286,247<br>Deaths: 2,463<br>Recovered: 278,650<br>Active: 5,134<br>Tests: 2,462,480<br>Doses: 1,307,993</p><p><a href=\"https://mastodon.cloud/tags/covid_north_dakota\" class=\"mention hashtag\" rel=\"nofollow noopener noreferrer\" target=\"_blank\">#<span>covid_north_dakota</span></a><br><a href=\"https://covid.yanoagenda.com/states/North%20Dakota\" rel=\"nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"ellipsis\">covid.yanoagenda.com/states/No</span><span class=\"invisible\">rth%20Dakota</span></a></p>";
const expected = '''
North Dakota 🏴 COVID-19 current stats for Sat Mar 18 2023
Cases: 286,247
Deaths: 2,463
Recovered: 278,650
Active: 5,134
Tests: 2,462,480
Doses: 1,307,993
#covid_north_dakota
https://covid.yanoagenda.com/states/North%20Dakota
''';
testConversion(original, expected);
});
// testPrint(bulletedListWithStuff);
// final nestedList =
// testPrint(nestedList);
test('Simple bulleted list', () {
const original =
"<p>Hello</p><ul class=\"listbullet\" style=\"list-style-type:circle;\"><li>bullet 1</li><li>bullet 2</li></ul>";
const expected = '''
Hello
- bullet 1
- bullet 2''';
testConversion(original, expected);
});
test('Heavily nested list', () {
const original =
"<p>List test</p><ul class=\"listbullet\" style=\"list-style-type:circle;\"><li>Level 1 a</li><li>Level 1 b <ul class=\"listbullet\" style=\"list-style-type:circle;\"><li>Level 2 a <ul class=\"listbullet\" style=\"list-style-type:circle;\"><li>Level 3 a</li><li>Level 3 b</li></ul></li><li>Level 2 b</li></ul></li></ul>";
const expected = '''
List test
- Level 1 a
- Level 1 b
- Level 2 a
- Level 3 a
- Level 3 b
- Level 2 b''';
testConversion(original, expected);
});
test('List with other HTML elements within', () {
const original =
"<p>Stuff in bulleted list</p><ul class=\"listbullet\" style=\"list-style-type:circle;\"><li>Text with <em>italics</em> <strong>bold</strong> <u>underline</u></li><li>A hyperlink! <a href=\"https://kotlinlang.org/\" target=\"_blank\" rel=\"noopener noreferrer\">kotlinlang.org/</a></li><li>Hashtag <a class=\"mention hashtag status-link\" href=\"https://friendicadevtest1.myportal.social/search?tag=hashtag\" rel=\"tag\">#<span>hashtag</span></a></li><li>Mention <a class=\"u-url mention status-link\" href=\"https://friendicadevtest1.myportal.social/profile/testuser3\" rel=\"noopener noreferrer\" target=\"_blank\" title=\"testuser3\">@<span>testuser3</span></a></li></ul>";
const expected = '''
Stuff in bulleted list
- Text with *italics* **bold** <u>underline</u>
- A hyperlink! https://kotlinlang.org/
- Hashtag #hashtag
- Mention @testuser3@friendicadevtest1.myportal.social''';
testConversion(original, expected);
});
}