mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2026-01-04 03:04:24 +00:00
feat: support pasting table from notion, google docs and google sheet (#7247)
* feat: support pasting table from Notion * test: add google docs / googles sheets table test * fix: google docs test * fix: paste table from notion test
This commit is contained in:
parent
187f7409ce
commit
8fdc6e9638
@ -1,15 +1,11 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy/shared/markdown_to_document.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:html2md/html2md.dart' as html2md;
|
||||
|
||||
extension PasteFromHtml on EditorState {
|
||||
Future<bool> pasteHtml(String html) async {
|
||||
final nodes = htmlToDocument(html).root.children.toList();
|
||||
// remove the front and back empty line
|
||||
while (nodes.isNotEmpty && nodes.first.delta?.isEmpty == true) {
|
||||
nodes.removeAt(0);
|
||||
}
|
||||
while (nodes.isNotEmpty && nodes.last.delta?.isEmpty == true) {
|
||||
nodes.removeLast();
|
||||
}
|
||||
final nodes = convertHtmlToNodes(html);
|
||||
// if there's no nodes being converted successfully, return false
|
||||
if (nodes.isEmpty) {
|
||||
return false;
|
||||
@ -21,4 +17,89 @@ extension PasteFromHtml on EditorState {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Convert the html to document nodes.
|
||||
// For the google docs table, it will be fallback to the markdown parser.
|
||||
List<Node> convertHtmlToNodes(String html) {
|
||||
List<Node> nodes = htmlToDocument(html).root.children.toList();
|
||||
|
||||
// 1. remove the front and back empty line
|
||||
while (nodes.isNotEmpty && nodes.first.delta?.isEmpty == true) {
|
||||
nodes.removeAt(0);
|
||||
}
|
||||
while (nodes.isNotEmpty && nodes.last.delta?.isEmpty == true) {
|
||||
nodes.removeLast();
|
||||
}
|
||||
|
||||
// 2. replace the legacy table nodes with the new simple table nodes
|
||||
for (int i = 0; i < nodes.length; i++) {
|
||||
final node = nodes[i];
|
||||
if (node.type == TableBlockKeys.type) {
|
||||
nodes[i] = _convertTableToSimpleTable(node);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. verify the nodes is empty or contains google table flag
|
||||
// The table from Google Docs will contain the flag 'Google Table'
|
||||
const googleDocsFlag = 'docs-internal-guid-';
|
||||
final isPasteFromGoogleDocs = html.contains(googleDocsFlag);
|
||||
if (nodes.isEmpty || isPasteFromGoogleDocs) {
|
||||
// fallback to the markdown parser
|
||||
final markdown = html2md.convert(html);
|
||||
nodes = customMarkdownToDocument(markdown).root.children.toList();
|
||||
}
|
||||
|
||||
// 4. check if the first node and the last node is bold, because google docs will wrap the table with bold tags
|
||||
if (isPasteFromGoogleDocs) {
|
||||
if (nodes.isNotEmpty && nodes.first.delta?.toPlainText() == '**') {
|
||||
nodes.removeAt(0);
|
||||
}
|
||||
if (nodes.isNotEmpty && nodes.last.delta?.toPlainText() == '**') {
|
||||
nodes.removeLast();
|
||||
}
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
// convert the legacy table node to the new simple table node
|
||||
// from type 'table' to type 'simple_table'
|
||||
Node _convertTableToSimpleTable(Node node) {
|
||||
if (node.type != TableBlockKeys.type) {
|
||||
return node;
|
||||
}
|
||||
|
||||
// the table node should contains colsLen and rowsLen
|
||||
final colsLen = node.attributes[TableBlockKeys.colsLen];
|
||||
final rowsLen = node.attributes[TableBlockKeys.rowsLen];
|
||||
if (colsLen == null || rowsLen == null) {
|
||||
return node;
|
||||
}
|
||||
|
||||
final rows = <List<Node>>[];
|
||||
final children = node.children;
|
||||
for (var i = 0; i < rowsLen; i++) {
|
||||
final row = <Node>[];
|
||||
for (var j = 0; j < colsLen; j++) {
|
||||
final cell = children
|
||||
.where(
|
||||
(n) =>
|
||||
n.attributes[TableCellBlockKeys.rowPosition] == i &&
|
||||
n.attributes[TableCellBlockKeys.colPosition] == j,
|
||||
)
|
||||
.firstOrNull;
|
||||
row.add(
|
||||
simpleTableCellBlockNode(
|
||||
children: cell?.children.map((e) => e.deepCopy()).toList() ??
|
||||
[paragraphNode()],
|
||||
),
|
||||
);
|
||||
}
|
||||
rows.add(row);
|
||||
}
|
||||
|
||||
return simpleTableBlockNode(
|
||||
children: rows.map((e) => simpleTableRowBlockNode(children: e)).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -998,6 +998,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.15.5"
|
||||
html2md:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: html2md
|
||||
sha256: "465cf8ffa1b510fe0e97941579bf5b22e2d575f2cecb500a9c0254efe33a8036"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.2"
|
||||
http:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
||||
@ -87,6 +87,7 @@ dependencies:
|
||||
highlight: ^0.7.0
|
||||
hive_flutter: ^1.1.0
|
||||
hotkey_manager: ^0.1.7
|
||||
html2md: ^1.3.2
|
||||
http: ^1.0.0
|
||||
image_picker: ^1.0.4
|
||||
|
||||
|
||||
@ -0,0 +1,361 @@
|
||||
// | Month | Savings |
|
||||
// | -------- | ------- |
|
||||
// | January | $250 |
|
||||
// | February | $80 |
|
||||
// | March | $420 |
|
||||
const tableFromNotion = '''
|
||||
<meta charset="utf-8" />
|
||||
<table id="18196b61-6923-80d7-a184-fbc0352dabc3" class="simple-table">
|
||||
<tbody>
|
||||
<tr id="18196b61-6923-80a7-b70a-cbae038e1472">
|
||||
<td id="Wi`b" class="">Month</td>
|
||||
<td id="|EyR" class="">Savings</td>
|
||||
</tr>
|
||||
<tr id="18196b61-6923-804a-914e-e45f6086a714">
|
||||
<td id="Wi`b" class="">January</td>
|
||||
<td id="|EyR" class="">\$250</td>
|
||||
</tr>
|
||||
<tr id="18196b61-6923-80b1-bef5-e15e1d302dfd">
|
||||
<td id="Wi`b" class="">February</td>
|
||||
<td id="|EyR" class="">\$80</td>
|
||||
</tr>
|
||||
<tr id="18196b61-6923-8079-aefa-d96c17230695">
|
||||
<td id="Wi`b" class="">March</td>
|
||||
<td id="|EyR" class="">\$420</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
''';
|
||||
|
||||
// | Month | Savings |
|
||||
// | -------- | ------- |
|
||||
// | January | $250 |
|
||||
// | February | $80 |
|
||||
// | March | $420 |
|
||||
const tableFromGoogleDocs = '''
|
||||
<meta charset="utf-8" /><meta charset="utf-8" />
|
||||
<b style="font-weight: normal;" id="docs-internal-guid-c1bfdd24-7fff-ad47-657e-45aa52e41e4c">
|
||||
<div dir="ltr" style="margin-left: 0pt;" align="left">
|
||||
<table style="border: none; border-collapse: collapse; table-layout: fixed; width: 451.27559055118115pt;">
|
||||
<colgroup>
|
||||
<col />
|
||||
<col />
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr style="height: 0pt;">
|
||||
<td
|
||||
style="
|
||||
border-left: solid #000000 1pt;
|
||||
border-right: solid #000000 1pt;
|
||||
border-bottom: solid #000000 1pt;
|
||||
border-top: solid #000000 1pt;
|
||||
vertical-align: top;
|
||||
padding: 5pt 5pt 5pt 5pt;
|
||||
overflow: hidden;
|
||||
overflow-wrap: break-word;
|
||||
"
|
||||
>
|
||||
<p dir="ltr" style="line-height: 1.2; margin-top: 0pt; margin-bottom: 0pt;">
|
||||
<span
|
||||
style="
|
||||
font-size: 11pt;
|
||||
font-family: Arial, sans-serif;
|
||||
color: #000000;
|
||||
background-color: transparent;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-variant: normal;
|
||||
text-decoration: none;
|
||||
vertical-align: baseline;
|
||||
white-space: pre;
|
||||
white-space: pre-wrap;
|
||||
"
|
||||
>
|
||||
Month
|
||||
</span>
|
||||
</p>
|
||||
</td>
|
||||
<td
|
||||
style="
|
||||
border-left: solid #000000 1pt;
|
||||
border-right: solid #000000 1pt;
|
||||
border-bottom: solid #000000 1pt;
|
||||
border-top: solid #000000 1pt;
|
||||
vertical-align: top;
|
||||
padding: 5pt 5pt 5pt 5pt;
|
||||
overflow: hidden;
|
||||
overflow-wrap: break-word;
|
||||
"
|
||||
>
|
||||
<p dir="ltr" style="line-height: 1.2; margin-top: 0pt; margin-bottom: 0pt;">
|
||||
<span
|
||||
style="
|
||||
font-size: 11pt;
|
||||
font-family: Arial, sans-serif;
|
||||
color: #000000;
|
||||
background-color: transparent;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-variant: normal;
|
||||
text-decoration: none;
|
||||
vertical-align: baseline;
|
||||
white-space: pre;
|
||||
white-space: pre-wrap;
|
||||
"
|
||||
>
|
||||
Savings
|
||||
</span>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="height: 0pt;">
|
||||
<td
|
||||
style="
|
||||
border-left: solid #000000 1pt;
|
||||
border-right: solid #000000 1pt;
|
||||
border-bottom: solid #000000 1pt;
|
||||
border-top: solid #000000 1pt;
|
||||
vertical-align: top;
|
||||
padding: 5pt 5pt 5pt 5pt;
|
||||
overflow: hidden;
|
||||
overflow-wrap: break-word;
|
||||
"
|
||||
>
|
||||
<p dir="ltr" style="line-height: 1.2; margin-top: 0pt; margin-bottom: 0pt;">
|
||||
<span
|
||||
style="
|
||||
font-size: 11pt;
|
||||
font-family: Arial, sans-serif;
|
||||
color: #000000;
|
||||
background-color: transparent;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-variant: normal;
|
||||
text-decoration: none;
|
||||
vertical-align: baseline;
|
||||
white-space: pre;
|
||||
white-space: pre-wrap;
|
||||
"
|
||||
>
|
||||
January
|
||||
</span>
|
||||
</p>
|
||||
</td>
|
||||
<td
|
||||
style="
|
||||
border-left: solid #000000 1pt;
|
||||
border-right: solid #000000 1pt;
|
||||
border-bottom: solid #000000 1pt;
|
||||
border-top: solid #000000 1pt;
|
||||
vertical-align: top;
|
||||
padding: 5pt 5pt 5pt 5pt;
|
||||
overflow: hidden;
|
||||
overflow-wrap: break-word;
|
||||
"
|
||||
>
|
||||
<p dir="ltr" style="line-height: 1.2; margin-top: 0pt; margin-bottom: 0pt;">
|
||||
<span
|
||||
style="
|
||||
font-size: 11pt;
|
||||
font-family: Arial, sans-serif;
|
||||
color: #000000;
|
||||
background-color: transparent;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-variant: normal;
|
||||
text-decoration: none;
|
||||
vertical-align: baseline;
|
||||
white-space: pre;
|
||||
white-space: pre-wrap;
|
||||
"
|
||||
>
|
||||
\$250
|
||||
</span>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="height: 0pt;">
|
||||
<td
|
||||
style="
|
||||
border-left: solid #000000 1pt;
|
||||
border-right: solid #000000 1pt;
|
||||
border-bottom: solid #000000 1pt;
|
||||
border-top: solid #000000 1pt;
|
||||
vertical-align: top;
|
||||
padding: 5pt 5pt 5pt 5pt;
|
||||
overflow: hidden;
|
||||
overflow-wrap: break-word;
|
||||
"
|
||||
>
|
||||
<p dir="ltr" style="line-height: 1.2; margin-top: 0pt; margin-bottom: 0pt;">
|
||||
<span
|
||||
style="
|
||||
font-size: 11pt;
|
||||
font-family: Arial, sans-serif;
|
||||
color: #000000;
|
||||
background-color: transparent;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-variant: normal;
|
||||
text-decoration: none;
|
||||
vertical-align: baseline;
|
||||
white-space: pre;
|
||||
white-space: pre-wrap;
|
||||
"
|
||||
>
|
||||
February
|
||||
</span>
|
||||
</p>
|
||||
</td>
|
||||
<td
|
||||
style="
|
||||
border-left: solid #000000 1pt;
|
||||
border-right: solid #000000 1pt;
|
||||
border-bottom: solid #000000 1pt;
|
||||
border-top: solid #000000 1pt;
|
||||
vertical-align: top;
|
||||
padding: 5pt 5pt 5pt 5pt;
|
||||
overflow: hidden;
|
||||
overflow-wrap: break-word;
|
||||
"
|
||||
>
|
||||
<p dir="ltr" style="line-height: 1.2; margin-top: 0pt; margin-bottom: 0pt;">
|
||||
<span
|
||||
style="
|
||||
font-size: 11pt;
|
||||
font-family: Arial, sans-serif;
|
||||
color: #000000;
|
||||
background-color: transparent;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-variant: normal;
|
||||
text-decoration: none;
|
||||
vertical-align: baseline;
|
||||
white-space: pre;
|
||||
white-space: pre-wrap;
|
||||
"
|
||||
>
|
||||
\$80
|
||||
</span>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="height: 0pt;">
|
||||
<td
|
||||
style="
|
||||
border-left: solid #000000 1pt;
|
||||
border-right: solid #000000 1pt;
|
||||
border-bottom: solid #000000 1pt;
|
||||
border-top: solid #000000 1pt;
|
||||
vertical-align: top;
|
||||
padding: 5pt 5pt 5pt 5pt;
|
||||
overflow: hidden;
|
||||
overflow-wrap: break-word;
|
||||
"
|
||||
>
|
||||
<p dir="ltr" style="line-height: 1.2; margin-top: 0pt; margin-bottom: 0pt;">
|
||||
<span
|
||||
style="
|
||||
font-size: 11pt;
|
||||
font-family: Arial, sans-serif;
|
||||
color: #000000;
|
||||
background-color: transparent;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-variant: normal;
|
||||
text-decoration: none;
|
||||
vertical-align: baseline;
|
||||
white-space: pre;
|
||||
white-space: pre-wrap;
|
||||
"
|
||||
>
|
||||
March
|
||||
</span>
|
||||
</p>
|
||||
</td>
|
||||
<td
|
||||
style="
|
||||
border-left: solid #000000 1pt;
|
||||
border-right: solid #000000 1pt;
|
||||
border-bottom: solid #000000 1pt;
|
||||
border-top: solid #000000 1pt;
|
||||
vertical-align: top;
|
||||
padding: 5pt 5pt 5pt 5pt;
|
||||
overflow: hidden;
|
||||
overflow-wrap: break-word;
|
||||
"
|
||||
>
|
||||
<p dir="ltr" style="line-height: 1.2; margin-top: 0pt; margin-bottom: 0pt;">
|
||||
<span
|
||||
style="
|
||||
font-size: 11pt;
|
||||
font-family: Arial, sans-serif;
|
||||
color: #000000;
|
||||
background-color: transparent;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-variant: normal;
|
||||
text-decoration: none;
|
||||
vertical-align: baseline;
|
||||
white-space: pre;
|
||||
white-space: pre-wrap;
|
||||
"
|
||||
>
|
||||
\$420
|
||||
</span>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</b>
|
||||
''';
|
||||
|
||||
// | Month | Savings |
|
||||
// | -------- | ------- |
|
||||
// | January | $250 |
|
||||
// | February | $80 |
|
||||
// | March | $420 |
|
||||
const tableFromGoogleSheets = '''
|
||||
<meta charset="utf-8" />
|
||||
<google-sheets-html-origin>
|
||||
<style type="text/css">
|
||||
<!--td {border: 1px solid #cccccc;}br {mso-data-placement:same-cell;}-->
|
||||
</style>
|
||||
<table
|
||||
xmlns="http://www.w3.org/1999/xhtml"
|
||||
cellspacing="0"
|
||||
cellpadding="0"
|
||||
dir="ltr"
|
||||
border="1"
|
||||
style="table-layout: fixed; font-size: 10pt; font-family: Arial; width: 0px; border-collapse: collapse; border: none;"
|
||||
data-sheets-root="1"
|
||||
data-sheets-baot="1"
|
||||
>
|
||||
<colgroup>
|
||||
<col width="100" />
|
||||
<col width="100" />
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr style="height: 21px;">
|
||||
<td style="overflow: hidden; padding: 2px 3px 2px 3px; vertical-align: bottom;">Month</td>
|
||||
<td style="overflow: hidden; padding: 2px 3px 2px 3px; vertical-align: bottom;">Savings</td>
|
||||
</tr>
|
||||
<tr style="height: 21px;">
|
||||
<td style="overflow: hidden; padding: 2px 3px 2px 3px; vertical-align: bottom;">January</td>
|
||||
<td style="overflow: hidden; padding: 2px 3px 2px 3px; vertical-align: bottom;">\$250</td>
|
||||
</tr>
|
||||
<tr style="height: 21px;">
|
||||
<td style="overflow: hidden; padding: 2px 3px 2px 3px; vertical-align: bottom;">February</td>
|
||||
<td style="overflow: hidden; padding: 2px 3px 2px 3px; vertical-align: bottom;">\$80</td>
|
||||
</tr>
|
||||
<tr style="height: 21px;">
|
||||
<td style="overflow: hidden; padding: 2px 3px 2px 3px; vertical-align: bottom;">March</td>
|
||||
<td style="overflow: hidden; padding: 2px 3px 2px 3px; vertical-align: bottom;">\$420</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</google-sheets-html-origin>
|
||||
|
||||
''';
|
||||
@ -0,0 +1,52 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_html.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_block_component.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '_html_samples.dart';
|
||||
|
||||
void main() {
|
||||
group('paste from html:', () {
|
||||
void checkTable(String html) {
|
||||
final nodes = EditorState.blank().convertHtmlToNodes(html);
|
||||
expect(nodes.length, 1);
|
||||
final table = nodes.first;
|
||||
expect(table.type, SimpleTableBlockKeys.type);
|
||||
expect(table.getCellText(0, 0), 'Month');
|
||||
expect(table.getCellText(0, 1), 'Savings');
|
||||
expect(table.getCellText(1, 0), 'January');
|
||||
expect(table.getCellText(1, 1), '\$250');
|
||||
expect(table.getCellText(2, 0), 'February');
|
||||
expect(table.getCellText(2, 1), '\$80');
|
||||
expect(table.getCellText(3, 0), 'March');
|
||||
expect(table.getCellText(3, 1), '\$420');
|
||||
}
|
||||
|
||||
test('sample 1 - paste table from Notion', () {
|
||||
checkTable(tableFromNotion);
|
||||
});
|
||||
|
||||
test('sample 2 - paste table from Google Docs', () {
|
||||
checkTable(tableFromGoogleDocs);
|
||||
});
|
||||
|
||||
test('sample 3 - paste table from Google Sheets', () {
|
||||
checkTable(tableFromGoogleSheets);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
extension on Node {
|
||||
String getCellText(
|
||||
int row,
|
||||
int column, {
|
||||
int index = 0,
|
||||
}) {
|
||||
return children[row]
|
||||
.children[column]
|
||||
.children[index]
|
||||
.delta
|
||||
?.toPlainText() ??
|
||||
'';
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user