Repository: zulip/zulip-flutter (language: dart)

Issue:
Handle strikethrough, the <del> element
We should handle the `<del>` element. This means "strikethrough" formatting; it's what you get with Markdown syntax like `~~this~~`, enclosing a span in `~~`.

Handling these means parsing them in `lib/model/content.dart`, and then styling them appropriately in `lib/widgets/content.dart`. For appropriate styling, see the Zulip web app's `web/styles/rendered_markdown.css`.



Discussion hints:
nan

Pull request: content: Handle strikethrough, the <del> element
This handles the `<del>` element. This means "strikethrough" formatting; it's what you get with Markdown syntax like `~~this~~`, enclosing a span in`~~`.

Handling these means:
- Parsing into `DeletedNode`
- Styling nodes that are of type `DeletedNode` with a line-through

Fixes #357

Final Result: 
![Simulator Screenshot - iPhone 15 Pro Max - 2024-03-25 at 06 28 41](https://github.com/zulip/zulip-flutter/assets/63330019/ff1ca7b6-4900-483f-82f2-4193ad477f74)


Test command: flutter test test/model/content_test.dart --reporter expanded 2>&1

Fix patch:
diff --git a/lib/model/content.dart b/lib/model/content.dart
index 05067c1d58..9156d0f51d 100644
--- a/lib/model/content.dart
+++ b/lib/model/content.dart
@@ -450,6 +450,10 @@ class StrongNode extends InlineContainerNode {
   const StrongNode({super.debugHtmlNode, required super.nodes});
 }
 
+class DeletedNode extends InlineContainerNode {
+  const DeletedNode({super.debugHtmlNode, required super.nodes});
+}
+
 class EmphasisNode extends InlineContainerNode {
   const EmphasisNode({super.debugHtmlNode, required super.nodes});
 }
@@ -717,6 +721,9 @@ class _ZulipContentParser {
     if (localName == 'strong' && className.isEmpty) {
       return StrongNode(nodes: nodes(), debugHtmlNode: debugHtmlNode);
     }
+    if (localName == 'del' && className.isEmpty) {
+      return DeletedNode(nodes: nodes(), debugHtmlNode: debugHtmlNode);
+    }
     if (localName == 'em' && className.isEmpty) {
       return EmphasisNode(nodes: nodes(), debugHtmlNode: debugHtmlNode);
     }
diff --git a/lib/widgets/content.dart b/lib/widgets/content.dart
index c023d6d7be..36ca7a6f9c 100644
--- a/lib/widgets/content.dart
+++ b/lib/widgets/content.dart
@@ -622,6 +622,8 @@ class _InlineContentBuilder {
       return const TextSpan(text: "");
     } else if (node is StrongNode) {
       return _buildStrong(node);
+    } else if (node is DeletedNode) {
+      return _buildDeleted(node);
     } else if (node is EmphasisNode) {
       return _buildEmphasis(node);
     } else if (node is LinkNode) {
@@ -653,6 +655,9 @@ class _InlineContentBuilder {
   InlineSpan _buildStrong(StrongNode node) => _buildNodes(node.nodes,
     style: weightVariableTextStyle(_context, wght: 600, wghtIfPlatformRequestsBold: 900));
 
+  InlineSpan _buildDeleted(DeletedNode node) => _buildNodes(node.nodes,
+    style: const TextStyle(decoration: TextDecoration.lineThrough));
+
   InlineSpan _buildEmphasis(EmphasisNode node) => _buildNodes(node.nodes,
     style: const TextStyle(fontStyle: FontStyle.italic));
 


Test patch:
diff --git a/test/model/content_test.dart b/test/model/content_test.dart
index a89277daf0..dbf213cebc 100644
--- a/test/model/content_test.dart
+++ b/test/model/content_test.dart
@@ -580,6 +580,11 @@ void main() {
     '<p><strong>bold</strong></p>',
     const StrongNode(nodes: [TextNode('bold')]));
 
+  testParseInline('parse deleted/strike-through',
+    // "~~strike through~~"
+    '<p><del>strike through</del></p>',
+    const DeletedNode(nodes: [TextNode('strike through')]));
+
   testParseInline('parse emphasis/italic',
     // "*italic*"
     '<p><em>italic</em></p>',
@@ -590,11 +595,12 @@ void main() {
     '<p><code>inline code</code></p>',
     const InlineCodeNode(nodes: [TextNode('inline code')]));
 
-  testParseInline('parse nested strong, em, code',
-    // "***`word`***"
-    '<p><strong><em><code>word</code></em></strong></p>',
-    const StrongNode(nodes: [EmphasisNode(nodes: [InlineCodeNode(nodes: [
-      TextNode('word')])])]));
+  testParseInline('parse nested del, strong, em, code',
+    // "~~***`word`***~~"
+    '<p><del><strong><em><code>word</code></em></strong></del></p>',
+    const DeletedNode(
+      nodes: [StrongNode(nodes: [EmphasisNode(nodes: [InlineCodeNode(nodes: [
+        TextNode('word')])])])]));
 
   group('LinkNode', () {
     testParseInline('parse link',
@@ -618,19 +624,21 @@ void main() {
         nodes: [TextNode('#mobile-team > zulip-flutter')]));
   });
 
-  testParseInline('parse nested link, strong, em, code',
-    // "[***`word`***](https://example/)"
-    '<p><a href="https://example/"><strong><em><code>word'
-        '</code></em></strong></a></p>',
+  testParseInline('parse nested link, del, strong, em, code',
+    // "[~~***`word`***~~](https://example/)"
+    '<p><a href="https://example/"><del><strong><em><code>word'
+        '</code></em></strong></del></a></p>',
     const LinkNode(url: 'https://example/',
-      nodes: [StrongNode(nodes: [EmphasisNode(nodes: [InlineCodeNode(nodes: [
-        TextNode('word')])])])]));
+      nodes: [DeletedNode(nodes: [
+        StrongNode(nodes: [EmphasisNode(nodes: [InlineCodeNode(nodes: [
+         TextNode('word')])])])])]));
 
-  testParseInline('parse nested strong, em, link',
-    // "***[t](/u)***"
-    '<p><strong><em><a href="/u">t</a></em></strong></p>',
-    const StrongNode(nodes: [EmphasisNode(nodes: [LinkNode(url: '/u',
-      nodes: [TextNode('t')])])]));
+  testParseInline('parse nested del, strong, em, link',
+    // "~~***[t](/u)***~~"
+    '<p><del><strong><em><a href="/u">t</a></em></strong></del></p>',
+    const DeletedNode(
+      nodes: [StrongNode(nodes: [EmphasisNode(nodes: [LinkNode(url: '/u',
+        nodes: [TextNode('t')])])])]));
 
   group('parse @-mentions', () {
     testParseInline('plain user @-mention',
@@ -727,14 +735,15 @@ void main() {
         HeadingNode(level: HeadingLevel.h6, links: null, nodes: [TextNode('six')])]);
 
     testParse('containing inline markup',
-      // "###### one [***`two`***](https://example/)"
-      '<h6>one <a href="https://example/"><strong><em><code>two'
-          '</code></em></strong></a></h6>', const [
+      // "###### one [**~~*`two`*~~**](https://example/)"
+      '<h6>one <a href="https://example/"><strong><del><em><code>two'
+          '</code></em></del></strong></a></h6>', const [
         HeadingNode(level: HeadingLevel.h6, links: null, nodes: [
           TextNode('one '),
           LinkNode(url: 'https://example/',
-            nodes: [StrongNode(nodes: [EmphasisNode(nodes: [
-              InlineCodeNode(nodes: [TextNode('two')])])])]),
+            nodes: [StrongNode(
+              nodes: [DeletedNode( nodes: [EmphasisNode(nodes: [
+                InlineCodeNode(nodes: [TextNode('two')])])])])]),
         ])]);
 
     testParse('amidst paragraphs',