mirror of
https://gitlab.com/mysocialportal/relatica
synced 2024-10-18 13:33:32 +00:00
Initial implementation of HTML Content to Editor conversion
This commit is contained in:
parent
328daa30a1
commit
4dbf21c656
3 changed files with 241 additions and 1 deletions
|
@ -20,6 +20,7 @@ import '../models/timeline_entry.dart';
|
||||||
import '../services/feature_version_checker.dart';
|
import '../services/feature_version_checker.dart';
|
||||||
import '../services/timeline_manager.dart';
|
import '../services/timeline_manager.dart';
|
||||||
import '../utils/active_profile_selector.dart';
|
import '../utils/active_profile_selector.dart';
|
||||||
|
import '../utils/html_to_edit_text_helper.dart';
|
||||||
import '../utils/snackbar_builder.dart';
|
import '../utils/snackbar_builder.dart';
|
||||||
|
|
||||||
class EditorScreen extends StatefulWidget {
|
class EditorScreen extends StatefulWidget {
|
||||||
|
@ -85,7 +86,7 @@ class _EditorScreenState extends State<EditorScreen> {
|
||||||
.andThenAsync((manager) async => await manager.getEntryById(widget.id));
|
.andThenAsync((manager) async => await manager.getEntryById(widget.id));
|
||||||
result.match(onSuccess: (entry) {
|
result.match(onSuccess: (entry) {
|
||||||
_logger.fine('Loading status ${widget.id} information into fields');
|
_logger.fine('Loading status ${widget.id} information into fields');
|
||||||
contentController.text = entry.body;
|
contentController.text = toEditTextField(entry.body);
|
||||||
spoilerController.text = entry.spoilerText;
|
spoilerController.text = entry.spoilerText;
|
||||||
existingMediaItems
|
existingMediaItems
|
||||||
.addAll(entry.mediaAttachments.map((e) => e.toImageEntry()));
|
.addAll(entry.mediaAttachments.map((e) => e.toImageEntry()));
|
||||||
|
|
115
lib/utils/html_to_edit_text_helper.dart
Normal file
115
lib/utils/html_to_edit_text_helper.dart
Normal file
|
@ -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('');
|
||||||
|
}
|
124
test/html_to_edit_text_helper_test.dart
Normal file
124
test/html_to_edit_text_helper_test.dart
Normal file
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in a new issue