From ee80fd5d972b4ed4e5c57824b4747c7250f1d85e Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Fri, 12 Aug 2022 15:26:34 +0800 Subject: [PATCH 1/5] feat: copy underline --- .../flowy_editor/lib/src/document/selection.dart | 7 +++++++ .../flowy_editor/lib/src/infra/html_converter.dart | 11 ++++++++++- .../copy_paste_handler.dart | 3 ++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/document/selection.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/document/selection.dart index 641a985577..3ac7293a2c 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/document/selection.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/document/selection.dart @@ -46,6 +46,13 @@ class Selection { (start.path <= end.path && !pathEquals(start.path, end.path)) || (isSingle && start.offset < end.offset); + Selection normalize() { + if (isForward) { + return Selection(start: end, end: start); + } + return this; + } + Selection get reversed => copyWith(start: end, end: start); Selection collapse({bool atStart = false}) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart index e16237c0fa..2ec4bd382e 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart @@ -18,6 +18,7 @@ const String tagParagraph = "p"; const String tagImage = "img"; const String tagAnchor = "a"; const String tagBold = "b"; +const String tagUnderline = "u"; const String tagStrong = "strong"; const String tagSpan = "span"; const String tagCode = "code"; @@ -54,7 +55,8 @@ class HTMLToNodesConverter { if (child.localName == tagAnchor || child.localName == tagSpan || child.localName == tagCode || - child.localName == tagStrong) { + child.localName == tagStrong || + child.localName == tagUnderline) { _handleRichTextElement(delta, child); } else if (child.localName == tagBold) { // Google docs wraps the the content inside the `` tag. @@ -203,6 +205,8 @@ class HTMLToNodesConverter { delta.insert(element.text, attributes); } else if (element.localName == tagStrong || element.localName == tagBold) { delta.insert(element.text, {"bold": true}); + } else if (element.localName == tagUnderline) { + delta.insert(element.text, {"underline": true}); } else { delta.insert(element.text); } @@ -454,6 +458,11 @@ class NodesToHTMLConverter { final strong = html.Element.tag(tagStrong); strong.append(html.Text(op.content)); childNodes.add(strong); + } else if (attributes.length == 1 && + attributes[StyleKey.underline] == true) { + final strong = html.Element.tag(tagUnderline); + strong.append(html.Text(op.content)); + childNodes.add(strong); } else { final span = html.Element.tag(tagSpan); final cssString = _attributesToCssStyle(attributes); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart index a5f392a4eb..363b84967a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart @@ -6,10 +6,11 @@ import 'package:flutter/services.dart'; import 'package:rich_clipboard/rich_clipboard.dart'; _handleCopy(EditorState editorState) async { - final selection = editorState.cursorSelection; + var selection = editorState.cursorSelection; if (selection == null || selection.isCollapsed) { return; } + selection = selection.normalize(); if (pathEquals(selection.start.path, selection.end.path)) { final nodeAtPath = editorState.document.nodeAtPath(selection.end.path)!; if (nodeAtPath.type == "text") { From 690ec5cc94c08070414090f606426d10f1627b64 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Fri, 12 Aug 2022 15:29:12 +0800 Subject: [PATCH 2/5] feat: italic --- .../flowy_editor/lib/src/infra/html_converter.dart | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart index 2ec4bd382e..f5f37f4c4e 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart @@ -17,6 +17,7 @@ const String tagList = "li"; const String tagParagraph = "p"; const String tagImage = "img"; const String tagAnchor = "a"; +const String tagItalic = "i"; const String tagBold = "b"; const String tagUnderline = "u"; const String tagStrong = "strong"; @@ -56,7 +57,8 @@ class HTMLToNodesConverter { child.localName == tagSpan || child.localName == tagCode || child.localName == tagStrong || - child.localName == tagUnderline) { + child.localName == tagUnderline || + child.localName == tagItalic) { _handleRichTextElement(delta, child); } else if (child.localName == tagBold) { // Google docs wraps the the content inside the `` tag. @@ -207,6 +209,8 @@ class HTMLToNodesConverter { delta.insert(element.text, {"bold": true}); } else if (element.localName == tagUnderline) { delta.insert(element.text, {"underline": true}); + } else if (element.localName == tagItalic) { + delta.insert(element.text, {"italic": true}); } else { delta.insert(element.text); } @@ -463,6 +467,11 @@ class NodesToHTMLConverter { final strong = html.Element.tag(tagUnderline); strong.append(html.Text(op.content)); childNodes.add(strong); + } else if (attributes.length == 1 && + attributes[StyleKey.italic] == true) { + final strong = html.Element.tag(tagItalic); + strong.append(html.Text(op.content)); + childNodes.add(strong); } else { final span = html.Element.tag(tagSpan); final cssString = _attributesToCssStyle(attributes); From 0f404e55274cb5ead4fe1c29f102c2022aceae6e Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Fri, 12 Aug 2022 15:31:34 +0800 Subject: [PATCH 3/5] feat: strike --- .../lib/src/infra/html_converter.dart | 49 ++++++++++++++++--- 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart index f5f37f4c4e..96c001a318 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart @@ -20,6 +20,7 @@ const String tagAnchor = "a"; const String tagItalic = "i"; const String tagBold = "b"; const String tagUnderline = "u"; +const String tagDel = "del"; const String tagStrong = "strong"; const String tagSpan = "span"; const String tagCode = "code"; @@ -58,7 +59,8 @@ class HTMLToNodesConverter { child.localName == tagCode || child.localName == tagStrong || child.localName == tagUnderline || - child.localName == tagItalic) { + child.localName == tagItalic || + child.localName == tagDel) { _handleRichTextElement(delta, child); } else if (child.localName == tagBold) { // Google docs wraps the the content inside the `` tag. @@ -132,7 +134,7 @@ class HTMLToNodesConverter { if (tuples.length < 2) { continue; } - result[tuples[0]] = tuples[1]; + result[tuples[0].trim()] = tuples[1].trim(); } return result; @@ -146,12 +148,23 @@ class HTMLToNodesConverter { final fontWeightStr = cssMap["font-weight"]; if (fontWeightStr != null) { - int? weight = int.tryParse(fontWeightStr); - if (weight != null && weight > 500) { - attrs["bold"] = true; + if (fontWeightStr == "bold") { + attrs[StyleKey.bold] = true; + } else { + int? weight = int.tryParse(fontWeightStr); + if (weight != null && weight > 500) { + attrs[StyleKey.bold] = true; + } } } + final textDecorationStr = cssMap["text-decoration"]; + if (textDecorationStr == "line-through") { + attrs[StyleKey.strikethrough] = true; + } else if (textDecorationStr == "underline") { + attrs[StyleKey.underline] = true; + } + final backgroundColorStr = cssMap["background-color"]; final backgroundColor = _tryParseCssColorString(backgroundColorStr); if (backgroundColor != null) { @@ -159,6 +172,10 @@ class HTMLToNodesConverter { '0x${backgroundColor.value.toRadixString(16)}'; } + if (cssMap["font-style"] == "italic") { + attrs[StyleKey.italic] = true; + } + return attrs.isEmpty ? null : attrs; } @@ -206,11 +223,13 @@ class HTMLToNodesConverter { } delta.insert(element.text, attributes); } else if (element.localName == tagStrong || element.localName == tagBold) { - delta.insert(element.text, {"bold": true}); + delta.insert(element.text, {StyleKey.bold: true}); } else if (element.localName == tagUnderline) { - delta.insert(element.text, {"underline": true}); + delta.insert(element.text, {StyleKey.underline: true}); } else if (element.localName == tagItalic) { - delta.insert(element.text, {"italic": true}); + delta.insert(element.text, {StyleKey.italic: true}); + } else if (element.localName == tagDel) { + delta.insert(element.text, {StyleKey.strikethrough: true}); } else { delta.insert(element.text); } @@ -422,6 +441,15 @@ class NodesToHTMLConverter { if (attributes[StyleKey.bold] == true) { cssMap["font-weight"] = "bold"; } + if (attributes[StyleKey.strikethrough] == true) { + cssMap["text-decoration"] = "line-through"; + } + if (attributes[StyleKey.underline] == true) { + cssMap["text-decoration"] = "underline"; + } + if (attributes[StyleKey.italic] == true) { + cssMap["font-style"] = "italic"; + } return _cssMapToCssStyle(cssMap); } @@ -472,6 +500,11 @@ class NodesToHTMLConverter { final strong = html.Element.tag(tagItalic); strong.append(html.Text(op.content)); childNodes.add(strong); + } else if (attributes.length == 1 && + attributes[StyleKey.strikethrough] == true) { + final strong = html.Element.tag(tagDel); + strong.append(html.Text(op.content)); + childNodes.add(strong); } else { final span = html.Element.tag(tagSpan); final cssString = _attributesToCssStyle(attributes); From 61aaa20113a16914593a4c3001098f8015764e86 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Fri, 12 Aug 2022 16:25:50 +0800 Subject: [PATCH 4/5] feat: handle text-decoration for text styles --- .../lib/src/infra/html_converter.dart | 39 ++++++++++++++----- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart index 96c001a318..cc7fd949ae 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart @@ -159,10 +159,8 @@ class HTMLToNodesConverter { } final textDecorationStr = cssMap["text-decoration"]; - if (textDecorationStr == "line-through") { - attrs[StyleKey.strikethrough] = true; - } else if (textDecorationStr == "underline") { - attrs[StyleKey.underline] = true; + if (textDecorationStr != null) { + _assignTextDecorations(attrs, textDecorationStr); } final backgroundColorStr = cssMap["background-color"]; @@ -179,6 +177,17 @@ class HTMLToNodesConverter { return attrs.isEmpty ? null : attrs; } + _assignTextDecorations(Attributes attrs, String decorationStr) { + final decorations = decorationStr.split(" "); + for (final d in decorations) { + if (d == "line-through") { + attrs[StyleKey.strikethrough] = true; + } else if (d == "underline") { + attrs[StyleKey.underline] = true; + } + } + } + /// Try to parse the `rgba(red, greed, blue, alpha)` /// from the string. Color? _tryParseCssColorString(String? colorString) { @@ -424,6 +433,18 @@ class NodesToHTMLConverter { checked: textNode.attributes["checkbox"] == true); } + String _textDecorationsFromAttributes(Attributes attributes) { + var textDecoration = []; + if (attributes[StyleKey.strikethrough] == true) { + textDecoration.add("line-through"); + } + if (attributes[StyleKey.underline] == true) { + textDecoration.add("underline"); + } + + return textDecoration.join(" "); + } + String _attributesToCssStyle(Map attributes) { final cssMap = {}; if (attributes[StyleKey.backgroundColor] != null) { @@ -441,12 +462,12 @@ class NodesToHTMLConverter { if (attributes[StyleKey.bold] == true) { cssMap["font-weight"] = "bold"; } - if (attributes[StyleKey.strikethrough] == true) { - cssMap["text-decoration"] = "line-through"; - } - if (attributes[StyleKey.underline] == true) { - cssMap["text-decoration"] = "underline"; + + final textDecoration = _textDecorationsFromAttributes(attributes); + if (textDecoration.isNotEmpty) { + cssMap["text-decoration"] = textDecoration; } + if (attributes[StyleKey.italic] == true) { cssMap["font-style"] = "italic"; } From 53cb05998bd3ae6f091a433830e0093176b97ffc Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Fri, 12 Aug 2022 16:37:43 +0800 Subject: [PATCH 5/5] feat(doc): HTML converter --- .../lib/src/infra/html_converter.dart | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart index cc7fd949ae..4a15b54859 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart @@ -484,6 +484,22 @@ class NodesToHTMLConverter { }); } + /// Convert the rich text to HTML + /// + /// Use `` for bold only. + /// Use `` for italic only. + /// Use `` for strikethrough only. + /// Use `` for underline only. + /// + /// If the text has multiple styles, use a `` + /// to mix the styles. + /// + /// A CSS style string is used to describe the styles. + /// The HTML will be: + /// + /// ```html + /// Text + /// ``` html.Element _deltaToHtml(Delta delta, {String? subType, int? end, bool? checked}) { if (end != null) {