mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge remote-tracking branch 'origin/main' into feat/toolbar_service
This commit is contained in:
commit
70853b918e
@ -90,8 +90,7 @@ class _ImageNodeWidgetState extends State<ImageNodeWidget> with Selectable {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Position getPositionInOffset(Offset start) {
|
Position getPositionInOffset(Offset start) {
|
||||||
// TODO: implement getPositionInOffset
|
return Position(path: node.path, offset: 0);
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -6,9 +6,13 @@
|
|||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <rich_clipboard_linux/rich_clipboard_plugin.h>
|
||||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
|
g_autoptr(FlPluginRegistrar) rich_clipboard_linux_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "RichClipboardPlugin");
|
||||||
|
rich_clipboard_plugin_register_with_registrar(rich_clipboard_linux_registrar);
|
||||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
rich_clipboard_linux
|
||||||
url_launcher_linux
|
url_launcher_linux
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -5,8 +5,10 @@
|
|||||||
import FlutterMacOS
|
import FlutterMacOS
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
import rich_clipboard_macos
|
||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
|
RichClipboardPlugin.register(with: registry.registrar(forPlugin: "RichClipboardPlugin"))
|
||||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,26 @@
|
|||||||
PODS:
|
PODS:
|
||||||
- FlutterMacOS (1.0.0)
|
- FlutterMacOS (1.0.0)
|
||||||
|
- rich_clipboard_macos (0.0.1):
|
||||||
|
- FlutterMacOS
|
||||||
- url_launcher_macos (0.0.1):
|
- url_launcher_macos (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||||
|
- rich_clipboard_macos (from `Flutter/ephemeral/.symlinks/plugins/rich_clipboard_macos/macos`)
|
||||||
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
FlutterMacOS:
|
FlutterMacOS:
|
||||||
:path: Flutter/ephemeral
|
:path: Flutter/ephemeral
|
||||||
|
rich_clipboard_macos:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/rich_clipboard_macos/macos
|
||||||
url_launcher_macos:
|
url_launcher_macos:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424
|
FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424
|
||||||
|
rich_clipboard_macos: 43364b66b9dc69d203eb8dd6d758e2d12e02723c
|
||||||
url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3
|
url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3
|
||||||
|
|
||||||
PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c
|
PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c
|
||||||
|
@ -43,6 +43,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.16.0"
|
version: "1.16.0"
|
||||||
|
csslib:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: csslib
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.17.2"
|
||||||
cupertino_icons:
|
cupertino_icons:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -57,6 +64,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0"
|
version: "1.3.0"
|
||||||
|
ffi:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: ffi
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.1"
|
||||||
flowy_editor:
|
flowy_editor:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -100,6 +114,13 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
html:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: html
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.15.0"
|
||||||
js:
|
js:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -184,6 +205,62 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.3"
|
version: "6.0.3"
|
||||||
|
rich_clipboard:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: rich_clipboard
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
|
rich_clipboard_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: rich_clipboard_android
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
|
rich_clipboard_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: rich_clipboard_ios
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
|
rich_clipboard_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: rich_clipboard_linux
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
|
rich_clipboard_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: rich_clipboard_macos
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.1"
|
||||||
|
rich_clipboard_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: rich_clipboard_platform_interface
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
|
rich_clipboard_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: rich_clipboard_web
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
|
rich_clipboard_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: rich_clipboard_windows
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -294,6 +371,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.2"
|
version: "2.1.2"
|
||||||
|
win32:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: win32
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.6.1"
|
||||||
xml:
|
xml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -303,4 +387,4 @@ packages:
|
|||||||
version: "6.1.0"
|
version: "6.1.0"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=2.17.0 <3.0.0"
|
dart: ">=2.17.0 <3.0.0"
|
||||||
flutter: ">=2.11.0-0.1.pre"
|
flutter: ">=3.0.0"
|
||||||
|
@ -176,10 +176,11 @@ class TextNode extends Node {
|
|||||||
|
|
||||||
TextNode({
|
TextNode({
|
||||||
required super.type,
|
required super.type,
|
||||||
required super.children,
|
|
||||||
required super.attributes,
|
|
||||||
required Delta delta,
|
required Delta delta,
|
||||||
}) : _delta = delta;
|
LinkedList<Node>? children,
|
||||||
|
Attributes? attributes,
|
||||||
|
}) : _delta = delta,
|
||||||
|
super(children: children ?? LinkedList(), attributes: attributes ?? {});
|
||||||
|
|
||||||
TextNode.empty()
|
TextNode.empty()
|
||||||
: _delta = Delta([TextInsert(' ')]),
|
: _delta = Delta([TextInsert(' ')]),
|
||||||
|
@ -0,0 +1,74 @@
|
|||||||
|
import 'package:flowy_editor/document/node.dart';
|
||||||
|
|
||||||
|
import './state_tree.dart';
|
||||||
|
import './node.dart';
|
||||||
|
|
||||||
|
/// [NodeIterator] is used to traverse the nodes in visual order.
|
||||||
|
class NodeIterator implements Iterator<Node> {
|
||||||
|
final StateTree stateTree;
|
||||||
|
final Node _startNode;
|
||||||
|
final Node? _endNode;
|
||||||
|
Node? _currentNode;
|
||||||
|
bool _began = false;
|
||||||
|
|
||||||
|
NodeIterator(this.stateTree, Node startNode, [Node? endNode])
|
||||||
|
: _startNode = startNode,
|
||||||
|
_endNode = endNode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool moveNext() {
|
||||||
|
if (!_began) {
|
||||||
|
_currentNode = _startNode;
|
||||||
|
_began = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
final node = _currentNode;
|
||||||
|
if (node == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_endNode != null && _endNode == node) {
|
||||||
|
_currentNode = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.children.isNotEmpty) {
|
||||||
|
_currentNode = _findLeadingChild(node);
|
||||||
|
} else if (node.next != null) {
|
||||||
|
_currentNode = node.next!;
|
||||||
|
} else {
|
||||||
|
final parent = node.parent!;
|
||||||
|
final nextOfParent = parent.next;
|
||||||
|
if (nextOfParent == null) {
|
||||||
|
_currentNode = null;
|
||||||
|
} else {
|
||||||
|
_currentNode = _findLeadingChild(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _currentNode != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Node _findLeadingChild(Node node) {
|
||||||
|
while (node.children.isNotEmpty) {
|
||||||
|
node = node.children.first;
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Node get current {
|
||||||
|
return _currentNode!;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Node> toList() {
|
||||||
|
final result = <Node>[];
|
||||||
|
|
||||||
|
while (moveNext()) {
|
||||||
|
result.add(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,201 @@
|
|||||||
|
import 'dart:collection';
|
||||||
|
|
||||||
|
import 'package:flowy_editor/document/attributes.dart';
|
||||||
|
import 'package:flowy_editor/document/node.dart';
|
||||||
|
import 'package:flowy_editor/document/text_delta.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:html/parser.dart' show parse;
|
||||||
|
import 'package:html/dom.dart' as html;
|
||||||
|
|
||||||
|
const String tagH1 = "h1";
|
||||||
|
const String tagH2 = "h2";
|
||||||
|
const String tagH3 = "h3";
|
||||||
|
const String tagUnorderedList = "ul";
|
||||||
|
const String tagList = "li";
|
||||||
|
const String tagParagraph = "p";
|
||||||
|
const String tagImage = "img";
|
||||||
|
const String tagAnchor = "a";
|
||||||
|
const String tagBold = "b";
|
||||||
|
const String tagStrong = "strong";
|
||||||
|
const String tagSpan = "span";
|
||||||
|
|
||||||
|
class HTMLConverter {
|
||||||
|
final html.Document _document;
|
||||||
|
|
||||||
|
HTMLConverter(String htmlString) : _document = parse(htmlString);
|
||||||
|
|
||||||
|
List<Node> toNodes() {
|
||||||
|
final result = <Node>[];
|
||||||
|
final delta = Delta();
|
||||||
|
|
||||||
|
final childNodes = _document.body?.nodes.toList() ?? <html.Node>[];
|
||||||
|
for (final child in childNodes) {
|
||||||
|
if (child is html.Element) {
|
||||||
|
if (child.localName == tagAnchor ||
|
||||||
|
child.localName == tagSpan ||
|
||||||
|
child.localName == tagStrong ||
|
||||||
|
child.localName == tagBold) {
|
||||||
|
_handleRichTextElement(delta, child);
|
||||||
|
} else {
|
||||||
|
_handleElement(result, child);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
delta.insert(child.text ?? "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delta.operations.isNotEmpty) {
|
||||||
|
result.add(TextNode(type: "text", delta: delta));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleElement(List<Node> nodes, html.Element element) {
|
||||||
|
if (element.localName == tagH1) {
|
||||||
|
_handleHeadingElement(nodes, element, tagH1);
|
||||||
|
} else if (element.localName == tagH2) {
|
||||||
|
_handleHeadingElement(nodes, element, tagH2);
|
||||||
|
} else if (element.localName == tagH3) {
|
||||||
|
_handleHeadingElement(nodes, element, tagH3);
|
||||||
|
} else if (element.localName == tagUnorderedList) {
|
||||||
|
_handleUnorderedList(nodes, element);
|
||||||
|
} else if (element.localName == tagList) {
|
||||||
|
_handleListElement(nodes, element);
|
||||||
|
} else if (element.localName == tagParagraph) {
|
||||||
|
_handleParagraph(nodes, element);
|
||||||
|
} else {
|
||||||
|
final delta = Delta();
|
||||||
|
delta.insert(element.text);
|
||||||
|
if (delta.operations.isNotEmpty) {
|
||||||
|
nodes.add(TextNode(type: "text", delta: delta));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleParagraph(List<Node> nodes, html.Element element) {
|
||||||
|
_handleRichText(nodes, element);
|
||||||
|
}
|
||||||
|
|
||||||
|
Attributes? _getDeltaAttributesFromHtmlAttributes(
|
||||||
|
LinkedHashMap<Object, String> htmlAttributes) {
|
||||||
|
final attrs = <String, dynamic>{};
|
||||||
|
final styleString = htmlAttributes["style"];
|
||||||
|
if (styleString != null) {
|
||||||
|
final entries = styleString.split(";");
|
||||||
|
for (final entry in entries) {
|
||||||
|
final tuples = entry.split(":");
|
||||||
|
if (tuples.length < 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (tuples[0] == "font-weight") {
|
||||||
|
int? weight = int.tryParse(tuples[1]);
|
||||||
|
if (weight != null && weight > 500) {
|
||||||
|
attrs["bold"] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return attrs.isEmpty ? null : attrs;
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleRichTextElement(Delta delta, html.Element element) {
|
||||||
|
if (element.localName == tagSpan) {
|
||||||
|
delta.insert(element.text,
|
||||||
|
_getDeltaAttributesFromHtmlAttributes(element.attributes));
|
||||||
|
} else if (element.localName == tagAnchor) {
|
||||||
|
final hyperLink = element.attributes["href"];
|
||||||
|
Map<String, dynamic>? attributes;
|
||||||
|
if (hyperLink != null) {
|
||||||
|
attributes = {"href": hyperLink};
|
||||||
|
}
|
||||||
|
delta.insert(element.text, attributes);
|
||||||
|
} else if (element.localName == tagStrong || element.localName == tagBold) {
|
||||||
|
delta.insert(element.text, {"bold": true});
|
||||||
|
} else {
|
||||||
|
delta.insert(element.text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleRichText(List<Node> nodes, html.Element element) {
|
||||||
|
final image = element.querySelector(tagImage);
|
||||||
|
if (image != null) {
|
||||||
|
_handleImage(nodes, image);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var delta = Delta();
|
||||||
|
|
||||||
|
for (final child in element.nodes.toList()) {
|
||||||
|
if (child is html.Element) {
|
||||||
|
_handleRichTextElement(delta, child);
|
||||||
|
} else {
|
||||||
|
delta.insert(child.text ?? "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delta.operations.isNotEmpty) {
|
||||||
|
nodes.add(TextNode(type: "text", delta: delta));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleImage(List<Node> nodes, html.Element element) {
|
||||||
|
final src = element.attributes["src"];
|
||||||
|
final attributes = <String, dynamic>{};
|
||||||
|
if (src != null) {
|
||||||
|
attributes["image_src"] = src;
|
||||||
|
}
|
||||||
|
debugPrint("insert image: $src");
|
||||||
|
nodes.add(
|
||||||
|
Node(type: "image", attributes: attributes, children: LinkedList()));
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleUnorderedList(List<Node> nodes, html.Element element) {
|
||||||
|
element.children.forEach((child) {
|
||||||
|
_handleListElement(nodes, child);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleHeadingElement(
|
||||||
|
List<Node> nodes,
|
||||||
|
html.Element element,
|
||||||
|
String headingStyle,
|
||||||
|
) {
|
||||||
|
final delta = Delta();
|
||||||
|
delta.insert(element.text);
|
||||||
|
if (delta.operations.isNotEmpty) {
|
||||||
|
nodes.add(TextNode(
|
||||||
|
type: "text",
|
||||||
|
attributes: {"subtype": "heading", "heading": headingStyle},
|
||||||
|
delta: delta));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleListElement(List<Node> nodes, html.Element element) {
|
||||||
|
final childNodes = element.nodes.toList();
|
||||||
|
for (final child in childNodes) {
|
||||||
|
if (child is html.Element) {
|
||||||
|
_handleRichText(nodes, child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String deltaToHtml(Delta delta) {
|
||||||
|
var result = "<p>";
|
||||||
|
|
||||||
|
for (final op in delta.operations) {
|
||||||
|
if (op is TextInsert) {
|
||||||
|
final attributes = op.attributes;
|
||||||
|
if (attributes != null && attributes["bold"] == true) {
|
||||||
|
result += '<strong>${op.content}</strong>';
|
||||||
|
} else {
|
||||||
|
result += op.content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result += "</p>";
|
||||||
|
return result;
|
||||||
|
}
|
@ -80,6 +80,10 @@ class TransactionBuilder {
|
|||||||
add(TextEditOperation(path, delta, inverted));
|
add(TextEditOperation(path, delta, inverted));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setAfterSelection(Selection sel) {
|
||||||
|
afterSelection = sel;
|
||||||
|
}
|
||||||
|
|
||||||
mergeText(TextNode firstNode, TextNode secondNode,
|
mergeText(TextNode firstNode, TextNode secondNode,
|
||||||
{int? firstOffset, int secondOffset = 0}) {
|
{int? firstOffset, int secondOffset = 0}) {
|
||||||
final firstLength = firstNode.delta.length;
|
final firstLength = firstNode.delta.length;
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import 'package:flutter/gestures.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flowy_editor/editor_state.dart';
|
import 'package:flowy_editor/editor_state.dart';
|
||||||
@ -11,6 +10,7 @@ import 'package:flowy_editor/render/rich_text/number_list_text.dart';
|
|||||||
import 'package:flowy_editor/render/rich_text/quoted_text.dart';
|
import 'package:flowy_editor/render/rich_text/quoted_text.dart';
|
||||||
import 'package:flowy_editor/service/input_service.dart';
|
import 'package:flowy_editor/service/input_service.dart';
|
||||||
import 'package:flowy_editor/service/internal_key_event_handlers/arrow_keys_handler.dart';
|
import 'package:flowy_editor/service/internal_key_event_handlers/arrow_keys_handler.dart';
|
||||||
|
import 'package:flowy_editor/service/internal_key_event_handlers/copy_paste_handler.dart';
|
||||||
import 'package:flowy_editor/service/internal_key_event_handlers/delete_nodes_handler.dart';
|
import 'package:flowy_editor/service/internal_key_event_handlers/delete_nodes_handler.dart';
|
||||||
import 'package:flowy_editor/service/internal_key_event_handlers/delete_text_handler.dart';
|
import 'package:flowy_editor/service/internal_key_event_handlers/delete_text_handler.dart';
|
||||||
import 'package:flowy_editor/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart';
|
import 'package:flowy_editor/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart';
|
||||||
@ -37,6 +37,7 @@ List<FlowyKeyEventHandler> defaultKeyEventHandler = [
|
|||||||
slashShortcutHandler,
|
slashShortcutHandler,
|
||||||
flowyDeleteNodesHandler,
|
flowyDeleteNodesHandler,
|
||||||
arrowKeysHandler,
|
arrowKeysHandler,
|
||||||
|
copyPasteKeysHandler,
|
||||||
enterInEdgeOfTextNodeHandler,
|
enterInEdgeOfTextNodeHandler,
|
||||||
updateTextStyleByCommandXHandler,
|
updateTextStyleByCommandXHandler,
|
||||||
];
|
];
|
||||||
@ -70,7 +71,6 @@ class _FlowyEditorState extends State<FlowyEditor> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
_scrollController = ScrollController()..addListener(_scrollCallback);
|
|
||||||
editorState.service.renderPluginService = _createRenderPlugin();
|
editorState.service.renderPluginService = _createRenderPlugin();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,8 +131,4 @@ class _FlowyEditorState extends State<FlowyEditor> {
|
|||||||
...widget.customBuilders,
|
...widget.customBuilders,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
void _scrollCallback() {
|
|
||||||
debugPrint('scrolling');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,233 @@
|
|||||||
|
import 'package:flowy_editor/flowy_editor.dart';
|
||||||
|
import 'package:flowy_editor/service/keyboard_service.dart';
|
||||||
|
import 'package:flowy_editor/infra/html_converter.dart';
|
||||||
|
import 'package:flowy_editor/document/node_iterator.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:rich_clipboard/rich_clipboard.dart';
|
||||||
|
|
||||||
|
_handleCopy(EditorState editorState) async {
|
||||||
|
final selection = editorState.cursorSelection;
|
||||||
|
if (selection == null || selection.isCollapsed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (pathEquals(selection.start.path, selection.end.path)) {
|
||||||
|
final nodeAtPath = editorState.document.nodeAtPath(selection.end.path)!;
|
||||||
|
if (nodeAtPath.type == "text") {
|
||||||
|
final textNode = nodeAtPath as TextNode;
|
||||||
|
final delta =
|
||||||
|
textNode.delta.slice(selection.start.offset, selection.end.offset);
|
||||||
|
|
||||||
|
final htmlString = deltaToHtml(delta);
|
||||||
|
debugPrint('copy html: $htmlString');
|
||||||
|
RichClipboard.setData(RichClipboardData(html: htmlString));
|
||||||
|
} else {
|
||||||
|
debugPrint("unimplemented: copy non-text");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final beginNode = editorState.document.nodeAtPath(selection.start.path)!;
|
||||||
|
final endNode = editorState.document.nodeAtPath(selection.end.path)!;
|
||||||
|
final traverser = NodeIterator(editorState.document, beginNode, endNode);
|
||||||
|
|
||||||
|
var copyString = "";
|
||||||
|
while (traverser.moveNext()) {
|
||||||
|
final node = traverser.current;
|
||||||
|
if (node.type == "text") {
|
||||||
|
final textNode = node as TextNode;
|
||||||
|
if (node == beginNode) {
|
||||||
|
final htmlString =
|
||||||
|
deltaToHtml(textNode.delta.slice(selection.start.offset));
|
||||||
|
copyString += htmlString;
|
||||||
|
} else if (node == endNode) {
|
||||||
|
final htmlString =
|
||||||
|
deltaToHtml(textNode.delta.slice(0, selection.end.offset));
|
||||||
|
copyString += htmlString;
|
||||||
|
} else {
|
||||||
|
final htmlString = deltaToHtml(textNode.delta);
|
||||||
|
copyString += htmlString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: handle image and other blocks
|
||||||
|
|
||||||
|
}
|
||||||
|
debugPrint('copy html: $copyString');
|
||||||
|
RichClipboard.setData(RichClipboardData(html: copyString));
|
||||||
|
}
|
||||||
|
|
||||||
|
_pasteHTML(EditorState editorState, String html) {
|
||||||
|
final selection = editorState.cursorSelection;
|
||||||
|
if (selection == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final path = [...selection.end.path];
|
||||||
|
if (path.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint('paste html: $html');
|
||||||
|
final converter = HTMLConverter(html);
|
||||||
|
final nodes = converter.toNodes();
|
||||||
|
|
||||||
|
if (nodes.isEmpty) {
|
||||||
|
return;
|
||||||
|
} else if (nodes.length == 1) {
|
||||||
|
final firstNode = nodes[0];
|
||||||
|
final nodeAtPath = editorState.document.nodeAtPath(path)!;
|
||||||
|
final tb = TransactionBuilder(editorState);
|
||||||
|
final startOffset = selection.start.offset;
|
||||||
|
if (nodeAtPath.type == "text" && firstNode.type == "text") {
|
||||||
|
final textNodeAtPath = nodeAtPath as TextNode;
|
||||||
|
final firstTextNode = firstNode as TextNode;
|
||||||
|
tb.textEdit(textNodeAtPath,
|
||||||
|
() => Delta().retain(startOffset).concat(firstTextNode.delta));
|
||||||
|
tb.setAfterSelection(Selection.collapsed(Position(
|
||||||
|
path: path, offset: startOffset + firstTextNode.delta.length)));
|
||||||
|
tb.commit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_pasteMultipleLinesInText(editorState, path, selection.start.offset, nodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
_pasteMultipleLinesInText(
|
||||||
|
EditorState editorState, List<int> path, int offset, List<Node> nodes) {
|
||||||
|
final tb = TransactionBuilder(editorState);
|
||||||
|
|
||||||
|
final firstNode = nodes[0];
|
||||||
|
final nodeAtPath = editorState.document.nodeAtPath(path)!;
|
||||||
|
|
||||||
|
if (nodeAtPath.type == "text" && firstNode.type == "text") {
|
||||||
|
// split and merge
|
||||||
|
final textNodeAtPath = nodeAtPath as TextNode;
|
||||||
|
final firstTextNode = firstNode as TextNode;
|
||||||
|
final remain = textNodeAtPath.delta.slice(offset);
|
||||||
|
|
||||||
|
tb.textEdit(
|
||||||
|
textNodeAtPath,
|
||||||
|
() => Delta()
|
||||||
|
.retain(offset)
|
||||||
|
.delete(remain.length)
|
||||||
|
.concat(firstTextNode.delta));
|
||||||
|
|
||||||
|
final tailNodes = nodes.sublist(1);
|
||||||
|
path[path.length - 1]++;
|
||||||
|
if (tailNodes.isNotEmpty) {
|
||||||
|
if (tailNodes.last.type == "text") {
|
||||||
|
final tailTextNode = tailNodes.last as TextNode;
|
||||||
|
tailTextNode.delta = tailTextNode.delta.concat(remain);
|
||||||
|
} else if (remain.length > 0) {
|
||||||
|
tailNodes.add(TextNode(type: "text", delta: remain));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tailNodes.add(TextNode(type: "text", delta: remain));
|
||||||
|
}
|
||||||
|
|
||||||
|
tb.insertNodes(path, tailNodes);
|
||||||
|
tb.commit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
path[path.length - 1]++;
|
||||||
|
tb.insertNodes(path, nodes);
|
||||||
|
tb.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
_handlePaste(EditorState editorState) async {
|
||||||
|
final data = await RichClipboard.getData();
|
||||||
|
if (data.html != null) {
|
||||||
|
_pasteHTML(editorState, data.html!);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.text != null) {
|
||||||
|
_handlePastePlainText(editorState, data.text!);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_handlePastePlainText(EditorState editorState, String plainText) {
|
||||||
|
final selection = editorState.cursorSelection;
|
||||||
|
if (selection == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final lines = plainText
|
||||||
|
.split("\n")
|
||||||
|
.map((e) => e.replaceAll(RegExp(r'\r'), ""))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (lines.isEmpty) {
|
||||||
|
return;
|
||||||
|
} else if (lines.length == 1) {
|
||||||
|
final node =
|
||||||
|
editorState.document.nodeAtPath(selection.end.path)! as TextNode;
|
||||||
|
final beginOffset = selection.end.offset;
|
||||||
|
TransactionBuilder(editorState)
|
||||||
|
..textEdit(node, () => Delta().retain(beginOffset).insert(lines[0]))
|
||||||
|
..setAfterSelection(Selection.collapsed(Position(
|
||||||
|
path: selection.end.path, offset: beginOffset + lines[0].length)))
|
||||||
|
..commit();
|
||||||
|
} else {
|
||||||
|
final firstLine = lines[0];
|
||||||
|
final beginOffset = selection.end.offset;
|
||||||
|
final remains = lines.sublist(1);
|
||||||
|
|
||||||
|
final path = [...selection.end.path];
|
||||||
|
if (path.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final node =
|
||||||
|
editorState.document.nodeAtPath(selection.end.path)! as TextNode;
|
||||||
|
final insertedLineSuffix = node.delta.slice(beginOffset);
|
||||||
|
|
||||||
|
path[path.length - 1]++;
|
||||||
|
var index = 0;
|
||||||
|
final tb = TransactionBuilder(editorState);
|
||||||
|
final nodes = remains.map((e) {
|
||||||
|
if (index++ == remains.length - 1) {
|
||||||
|
return TextNode(
|
||||||
|
type: "text",
|
||||||
|
delta: Delta().insert(e).addAll(insertedLineSuffix.operations));
|
||||||
|
}
|
||||||
|
return TextNode(type: "text", delta: Delta().insert(e));
|
||||||
|
}).toList();
|
||||||
|
// insert first line
|
||||||
|
tb.textEdit(
|
||||||
|
node,
|
||||||
|
() => Delta()
|
||||||
|
.retain(beginOffset)
|
||||||
|
.insert(firstLine)
|
||||||
|
.delete(node.delta.length - beginOffset));
|
||||||
|
// insert remains
|
||||||
|
tb.insertNodes(path, nodes);
|
||||||
|
tb.commit();
|
||||||
|
|
||||||
|
// fixme: don't set the cursor manually
|
||||||
|
editorState.updateCursorSelection(Selection.collapsed(
|
||||||
|
Position(path: nodes.last.path, offset: lines.last.length)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleCut() {
|
||||||
|
debugPrint('cut');
|
||||||
|
}
|
||||||
|
|
||||||
|
FlowyKeyEventHandler copyPasteKeysHandler = (editorState, event) {
|
||||||
|
if (event.isMetaPressed && event.logicalKey == LogicalKeyboardKey.keyC) {
|
||||||
|
_handleCopy(editorState);
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
if (event.isMetaPressed && event.logicalKey == LogicalKeyboardKey.keyV) {
|
||||||
|
_handlePaste(editorState);
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
if (event.isMetaPressed && event.logicalKey == LogicalKeyboardKey.keyX) {
|
||||||
|
_handleCut();
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
};
|
@ -5,8 +5,10 @@ import 'package:flutter/gestures.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flowy_editor/document/node.dart';
|
import 'package:flowy_editor/document/node.dart';
|
||||||
|
import 'package:flowy_editor/document/node_iterator.dart';
|
||||||
import 'package:flowy_editor/document/position.dart';
|
import 'package:flowy_editor/document/position.dart';
|
||||||
import 'package:flowy_editor/document/selection.dart';
|
import 'package:flowy_editor/document/selection.dart';
|
||||||
|
import 'package:flowy_editor/document/state_tree.dart';
|
||||||
import 'package:flowy_editor/editor_state.dart';
|
import 'package:flowy_editor/editor_state.dart';
|
||||||
import 'package:flowy_editor/extensions/node_extensions.dart';
|
import 'package:flowy_editor/extensions/node_extensions.dart';
|
||||||
import 'package:flowy_editor/render/selection/cursor_widget.dart';
|
import 'package:flowy_editor/render/selection/cursor_widget.dart';
|
||||||
@ -129,7 +131,7 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
List<Node> getNodesInSelection(Selection selection) =>
|
List<Node> getNodesInSelection(Selection selection) =>
|
||||||
_selectedNodesInSelection(editorState.document.root, selection);
|
_selectedNodesInSelection(editorState.document, selection);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -381,7 +383,7 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
final selection = Selection(
|
final selection = Selection(
|
||||||
start: isDownward ? start : end, end: isDownward ? end : start);
|
start: isDownward ? start : end, end: isDownward ? end : start);
|
||||||
debugPrint('[_onPanUpdate] isDownward = $isDownward, $selection');
|
debugPrint('[_onPanUpdate] isDownward = $isDownward, $selection');
|
||||||
editorState.service.selectionService.updateSelection(selection);
|
editorState.updateCursorSelection(selection);
|
||||||
}
|
}
|
||||||
|
|
||||||
_scrollUpOrDownIfNeeded(panEndOffset!);
|
_scrollUpOrDownIfNeeded(panEndOffset!);
|
||||||
@ -393,8 +395,7 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _updateSelection(Selection selection) {
|
void _updateSelection(Selection selection) {
|
||||||
final nodes =
|
final nodes = _selectedNodesInSelection(editorState.document, selection);
|
||||||
_selectedNodesInSelection(editorState.document.root, selection);
|
|
||||||
|
|
||||||
currentSelection = selection;
|
currentSelection = selection;
|
||||||
currentSelectedNodes.value = nodes;
|
currentSelectedNodes.value = nodes;
|
||||||
@ -503,17 +504,11 @@ class _FlowySelectionState extends State<FlowySelection>
|
|||||||
currentState?.show();
|
currentState?.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Node> _selectedNodesInSelection(Node node, Selection selection) {
|
List<Node> _selectedNodesInSelection(
|
||||||
List<Node> result = [];
|
StateTree stateTree, Selection selection) {
|
||||||
if (node.parent != null) {
|
final startNode = stateTree.nodeAtPath(selection.start.path)!;
|
||||||
if (node.inSelection(selection)) {
|
final endNode = stateTree.nodeAtPath(selection.end.path)!;
|
||||||
result.add(node);
|
return NodeIterator(stateTree, startNode, endNode).toList();
|
||||||
}
|
|
||||||
}
|
|
||||||
for (final child in node.children) {
|
|
||||||
result.addAll(_selectedNodesInSelection(child, selection));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _scrollUpOrDownIfNeeded(Offset offset) {
|
void _scrollUpOrDownIfNeeded(Offset offset) {
|
||||||
|
@ -11,6 +11,8 @@ dependencies:
|
|||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
|
rich_clipboard: ^1.0.0
|
||||||
|
html: ^0.15.0
|
||||||
flutter_svg: ^1.1.1+1
|
flutter_svg: ^1.1.1+1
|
||||||
provider: ^6.0.3
|
provider: ^6.0.3
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ mod tests {
|
|||||||
let field_type = FieldType::URL;
|
let field_type = FieldType::URL;
|
||||||
let field_rev = FieldBuilder::from_field_type(&field_type).build();
|
let field_rev = FieldBuilder::from_field_type(&field_type).build();
|
||||||
assert_url(&type_option, "123", "123", "", &field_type, &field_rev);
|
assert_url(&type_option, "123", "123", "", &field_type, &field_rev);
|
||||||
|
assert_url(&type_option, "", "", "", &field_type, &field_rev);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The expected_str will equal to the input string, but the expected_url will not be empty
|
/// The expected_str will equal to the input string, but the expected_url will not be empty
|
||||||
@ -42,6 +43,124 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// if there's a http url and some words following it in the input string.
|
||||||
|
#[test]
|
||||||
|
fn url_type_option_contains_url_with_string_after_test() {
|
||||||
|
let type_option = URLTypeOption::default();
|
||||||
|
let field_type = FieldType::URL;
|
||||||
|
let field_rev = FieldBuilder::from_field_type(&field_type).build();
|
||||||
|
assert_url(
|
||||||
|
&type_option,
|
||||||
|
"AppFlowy website - https://www.appflowy.io welcome!",
|
||||||
|
"AppFlowy website - https://www.appflowy.io welcome!",
|
||||||
|
"https://www.appflowy.io/",
|
||||||
|
&field_type,
|
||||||
|
&field_rev,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_url(
|
||||||
|
&type_option,
|
||||||
|
"AppFlowy website appflowy.io welcome!",
|
||||||
|
"AppFlowy website appflowy.io welcome!",
|
||||||
|
"https://appflowy.io",
|
||||||
|
&field_type,
|
||||||
|
&field_rev,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// if there's a http url and special words following it in the input string.
|
||||||
|
#[test]
|
||||||
|
fn url_type_option_contains_url_with_special_string_after_test() {
|
||||||
|
let type_option = URLTypeOption::default();
|
||||||
|
let field_type = FieldType::URL;
|
||||||
|
let field_rev = FieldBuilder::from_field_type(&field_type).build();
|
||||||
|
assert_url(
|
||||||
|
&type_option,
|
||||||
|
"AppFlowy website - https://www.appflowy.io!",
|
||||||
|
"AppFlowy website - https://www.appflowy.io!",
|
||||||
|
"https://www.appflowy.io/",
|
||||||
|
&field_type,
|
||||||
|
&field_rev,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_url(
|
||||||
|
&type_option,
|
||||||
|
"AppFlowy website appflowy.io!",
|
||||||
|
"AppFlowy website appflowy.io!",
|
||||||
|
"https://appflowy.io",
|
||||||
|
&field_type,
|
||||||
|
&field_rev,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// if there's a level4 url in the input string.
|
||||||
|
#[test]
|
||||||
|
fn level4_url_type_test() {
|
||||||
|
let type_option = URLTypeOption::default();
|
||||||
|
let field_type = FieldType::URL;
|
||||||
|
let field_rev = FieldBuilder::from_field_type(&field_type).build();
|
||||||
|
assert_url(
|
||||||
|
&type_option,
|
||||||
|
"test - https://tester.testgroup.appflowy.io",
|
||||||
|
"test - https://tester.testgroup.appflowy.io",
|
||||||
|
"https://tester.testgroup.appflowy.io/",
|
||||||
|
&field_type,
|
||||||
|
&field_rev,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_url(
|
||||||
|
&type_option,
|
||||||
|
"test tester.testgroup.appflowy.io",
|
||||||
|
"test tester.testgroup.appflowy.io",
|
||||||
|
"https://tester.testgroup.appflowy.io",
|
||||||
|
&field_type,
|
||||||
|
&field_rev,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// urls with different top level domains.
|
||||||
|
#[test]
|
||||||
|
fn different_top_level_domains_test() {
|
||||||
|
let type_option = URLTypeOption::default();
|
||||||
|
let field_type = FieldType::URL;
|
||||||
|
let field_rev = FieldBuilder::from_field_type(&field_type).build();
|
||||||
|
assert_url(
|
||||||
|
&type_option,
|
||||||
|
"appflowy - https://appflowy.com",
|
||||||
|
"appflowy - https://appflowy.com",
|
||||||
|
"https://appflowy.com/",
|
||||||
|
&field_type,
|
||||||
|
&field_rev,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_url(
|
||||||
|
&type_option,
|
||||||
|
"appflowy - https://appflowy.top",
|
||||||
|
"appflowy - https://appflowy.top",
|
||||||
|
"https://appflowy.top/",
|
||||||
|
&field_type,
|
||||||
|
&field_rev,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_url(
|
||||||
|
&type_option,
|
||||||
|
"appflowy - https://appflowy.net",
|
||||||
|
"appflowy - https://appflowy.net",
|
||||||
|
"https://appflowy.net/",
|
||||||
|
&field_type,
|
||||||
|
&field_rev,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_url(
|
||||||
|
&type_option,
|
||||||
|
"appflowy - https://appflowy.edu",
|
||||||
|
"appflowy - https://appflowy.edu",
|
||||||
|
"https://appflowy.edu/",
|
||||||
|
&field_type,
|
||||||
|
&field_rev,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn assert_url(
|
fn assert_url(
|
||||||
type_option: &URLTypeOption,
|
type_option: &URLTypeOption,
|
||||||
input_str: &str,
|
input_str: &str,
|
||||||
|
Loading…
Reference in New Issue
Block a user