kopia lustrzana https://gitlab.com/mysocialportal/relatica
				
				
				
			Initial implementation of HTML Content to Editor conversion
							rodzic
							
								
									328daa30a1
								
							
						
					
					
						commit
						4dbf21c656
					
				| 
						 | 
				
			
			@ -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()));
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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('');
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
		Ładowanie…
	
		Reference in New Issue