diff --git a/frontend/app_flowy/packages/appflowy_editor/README.md b/frontend/app_flowy/packages/appflowy_editor/README.md
index a6f47172de..cdeec94dec 100644
--- a/frontend/app_flowy/packages/appflowy_editor/README.md
+++ b/frontend/app_flowy/packages/appflowy_editor/README.md
@@ -1,14 +1,14 @@
-
AppFlowy Editor
@@ -51,7 +51,7 @@ flutter pub get
## Creating Your First Editor
-Start by creating a new empty AppFlowyEditor object.
+Start by creating a new empty AppFlowyEditor object.
```dart
final editorState = EditorState.empty(); // an empty state
@@ -60,7 +60,7 @@ final editor = AppFlowyEditor(
);
```
-You can also create an editor from a JSON object in order to configure your initial state.
+You can also create an editor from a JSON object in order to configure your initial state. Or you can [create an editor from Markdown or Quill Delta](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/documentation/importing.md).
```dart
final json = ...;
@@ -79,7 +79,7 @@ MaterialApp(
);
```
-To get a sense for how the AppFlowy Editor works, run our example:
+To get a sense of how the AppFlowy Editor works, run our example:
```shell
git clone https://github.com/AppFlowy-IO/AppFlowy.git
@@ -98,7 +98,7 @@ Below are some examples of component customizations:
* [Checkbox Text](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart) demonstrates how to extend new styles based on existing rich text components
* [Image](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/network_image_node_widget.dart) demonstrates how to extend a new node and render it
* See further examples of [rich-text plugins](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text)
-
+
### Customizing Shortcut Events
Please refer to our documentation on customizing AppFlowy for a detailed discussion about [customizing shortcut events](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/documentation/customizing.md#customize-a-shortcut-event).
@@ -113,7 +113,7 @@ Below are some examples of shortcut event customizations:
Please refer to the API documentation.
## Contributing
-Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated.
+Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated.
Please look at [CONTRIBUTING.md](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/contributing-to-appflowy) for details.
diff --git a/frontend/app_flowy/packages/appflowy_editor/documentation/importing.md b/frontend/app_flowy/packages/appflowy_editor/documentation/importing.md
new file mode 100644
index 0000000000..abe682adef
--- /dev/null
+++ b/frontend/app_flowy/packages/appflowy_editor/documentation/importing.md
@@ -0,0 +1,36 @@
+# Importing data
+
+For now, we have supported three ways to import data to initialize AppFlowy Editor.
+
+1. From AppFlowy Document JSON
+
+```dart
+const document = r'''{"document":{"type":"editor","children":[{"type":"text","attributes":{"subtype":"heading","heading":"h1"},"delta":[{"insert":"Hello AppFlowy!"}]}]}}''';
+final json = jsonDecode(document);
+final editorState = EditorState(
+ document: Document.fromJson(
+ Map.from(json),
+ ),
+);
+```
+
+2. From Markdown
+
+```dart
+const markdown = r'''# Hello AppFlowy!''';
+final editorState = EditorState(
+ document: markdownToDocument(markdown),
+);
+```
+
+3. From Quill Delta
+
+```dart
+const delta = r'''[{"insert":"Hello AppFlowy!"},{"attributes":{"header":1},"insert":"\n"}]''';
+final json = jsonDecode(delta);
+final editorState = EditorState(
+ document: DeltaDocumentConvert().convertFromJSON(json),
+);
+```
+
+For more details, please refer to the function `_importFile` through this [link](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/example/lib/home_page.dart).
\ No newline at end of file
diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/home_page.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/home_page.dart
index 18d74655ed..c4b706c6f6 100644
--- a/frontend/app_flowy/packages/appflowy_editor/example/lib/home_page.dart
+++ b/frontend/app_flowy/packages/appflowy_editor/example/lib/home_page.dart
@@ -14,12 +14,14 @@ enum ExportFileType {
json,
markdown,
html,
+ delta,
}
extension on ExportFileType {
String get extension {
switch (this) {
case ExportFileType.json:
+ case ExportFileType.delta:
return 'json';
case ExportFileType.markdown:
return 'md';
@@ -117,6 +119,9 @@ class _HomePageState extends State {
_buildListTile(context, 'Import From Markdown', () {
_importFile(ExportFileType.markdown);
}),
+ _buildListTile(context, 'Import From Quill Delta', () {
+ _importFile(ExportFileType.delta);
+ }),
// Theme Demo
_buildSeparator(context, 'Theme Demo'),
@@ -224,6 +229,7 @@ class _HomePageState extends State {
result = documentToMarkdown(editorState.document);
break;
case ExportFileType.html:
+ case ExportFileType.delta:
throw UnimplementedError();
}
@@ -280,6 +286,17 @@ class _HomePageState extends State {
case ExportFileType.markdown:
jsonString = jsonEncode(markdownToDocument(plainText).toJson());
break;
+ case ExportFileType.delta:
+ jsonString = jsonEncode(
+ DeltaDocumentConvert()
+ .convertFromJSON(
+ jsonDecode(
+ plainText.replaceAll('\\\\\n', '\\n'),
+ ),
+ )
+ .toJson(),
+ );
+ break;
case ExportFileType.html:
throw UnimplementedError();
}
diff --git a/frontend/app_flowy/packages/appflowy_editor/example/macos/Podfile.lock b/frontend/app_flowy/packages/appflowy_editor/example/macos/Podfile.lock
index 1399b50219..971f196463 100644
--- a/frontend/app_flowy/packages/appflowy_editor/example/macos/Podfile.lock
+++ b/frontend/app_flowy/packages/appflowy_editor/example/macos/Podfile.lock
@@ -35,7 +35,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
flowy_infra_ui: c34d49d615ed9fe552cd47f90d7850815a74e9e9
- FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424
+ FlutterMacOS: ae6af50a8ea7d6103d888583d46bd8328a7e9811
path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19
rich_clipboard_macos: 43364b66b9dc69d203eb8dd6d758e2d12e02723c
shared_preferences_macos: a64dc611287ed6cbe28fd1297898db1336975727
diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart b/frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart
index 983badaaa5..cf5c4e2d75 100644
--- a/frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart
+++ b/frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart
@@ -41,3 +41,4 @@ export 'src/plugins/markdown/encoder/parser/text_node_parser.dart';
export 'src/plugins/markdown/encoder/parser/image_node_parser.dart';
export 'src/plugins/markdown/decoder/delta_markdown_decoder.dart';
export 'src/plugins/markdown/document_markdown.dart';
+export 'src/plugins/quill_delta/delta_document_encoder.dart';
diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/plugins/quill_delta/delta_document_encoder.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/plugins/quill_delta/delta_document_encoder.dart
new file mode 100644
index 0000000000..e2b71684d7
--- /dev/null
+++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/plugins/quill_delta/delta_document_encoder.dart
@@ -0,0 +1,232 @@
+import 'package:appflowy_editor/src/core/document/attributes.dart';
+import 'package:appflowy_editor/src/core/document/document.dart';
+import 'package:appflowy_editor/src/core/document/node.dart';
+import 'package:appflowy_editor/src/core/document/text_delta.dart';
+import 'package:appflowy_editor/src/core/legacy/built_in_attribute_keys.dart';
+import 'package:flutter/material.dart';
+
+class DeltaDocumentConvert {
+ DeltaDocumentConvert();
+
+ var _number = 1;
+ final Map> _bulletedList = {};
+
+ Document convertFromJSON(List json) {
+ final delta = Delta.fromJson(json);
+ return convertFromDelta(delta);
+ }
+
+ Document convertFromDelta(Delta delta) {
+ final iter = delta.iterator;
+
+ final document = Document.empty();
+ TextNode textNode = TextNode(delta: Delta());
+ int path = 0;
+
+ while (iter.moveNext()) {
+ final op = iter.current;
+ if (op is TextInsert) {
+ if (op.text != '\n') {
+ // Attributes associated with a newline character describes formatting for that line.
+ final texts = op.text.split('\n');
+ if (texts.length > 1) {
+ textNode.delta.insert(texts[0]);
+ document.insert([path++], [textNode]);
+ textNode = TextNode(delta: Delta()..insert(texts[1]));
+ } else {
+ _applyStyle(textNode, op.text, op.attributes);
+ }
+ } else {
+ if (!_containNumberListStyle(op.attributes)) {
+ _number = 1;
+ }
+ _applyListStyle(textNode, op.attributes);
+ _applyHeaderStyle(textNode, op.attributes);
+ _applyIndent(textNode, op.attributes);
+ _applyBlockquote(textNode, op.attributes);
+ // _applyCodeBlock(textNode, op.attributes);
+
+ if (_containIndentBulletedListStyle(op.attributes)) {
+ final level = _indentLevel(op.attributes);
+ final path = [
+ ..._bulletedList[level - 1]!.last.path,
+ _bulletedList[level]!.length - 1,
+ ];
+ document.insert(path, [textNode]);
+ } else {
+ document.insert([path++], [textNode]);
+ }
+ textNode = TextNode(delta: Delta());
+ }
+ } else {
+ assert(false, 'op must be TextInsert');
+ }
+ }
+
+ return document;
+ }
+
+ void _applyStyle(TextNode textNode, String text, Map? attributes) {
+ Attributes attrs = {};
+
+ if (_containsStyle(attributes, 'strike')) {
+ attrs[BuiltInAttributeKey.strikethrough] = true;
+ }
+ if (_containsStyle(attributes, 'underline')) {
+ attrs[BuiltInAttributeKey.underline] = true;
+ }
+ if (_containsStyle(attributes, 'bold')) {
+ attrs[BuiltInAttributeKey.bold] = true;
+ }
+ if (_containsStyle(attributes, 'italic')) {
+ attrs[BuiltInAttributeKey.italic] = true;
+ }
+ final link = attributes?['link'] as String?;
+ if (link != null) {
+ attrs[BuiltInAttributeKey.href] = link;
+ }
+ final color = attributes?['color'] as String?;
+ final colorHex = _convertColorToHexString(color);
+ if (colorHex != null) {
+ attrs[BuiltInAttributeKey.color] = colorHex;
+ }
+ final backgroundColor = attributes?['background'] as String?;
+ final backgroundHex = _convertColorToHexString(backgroundColor);
+ if (backgroundHex != null) {
+ attrs[BuiltInAttributeKey.backgroundColor] = backgroundHex;
+ }
+
+ textNode.delta.insert(text, attributes: attrs);
+ }
+
+ bool _containsStyle(Map? attributes, String key) {
+ final value = attributes?[key] as bool?;
+ return value == true;
+ }
+
+ String? _convertColorToHexString(String? color) {
+ if (color == null) {
+ return null;
+ }
+ if (color.startsWith('#')) {
+ return '0xFF${color.substring(1)}';
+ } else if (color.startsWith("rgba")) {
+ List rgbaList = color.substring(5, color.length - 1).split(',');
+ return Color.fromRGBO(
+ int.parse(rgbaList[0]),
+ int.parse(rgbaList[1]),
+ int.parse(rgbaList[2]),
+ double.parse(rgbaList[3]),
+ ).toHex();
+ }
+ return null;
+ }
+
+ // convert bullet-list, number-list, check-list to appflowy style list.
+ void _applyListStyle(TextNode textNode, Map? attributes) {
+ final indent = attributes?['indent'] as int?;
+ final list = attributes?['list'] as String?;
+ if (list != null) {
+ switch (list) {
+ case 'bullet':
+ textNode.updateAttributes({
+ BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList,
+ });
+ if (indent != null) {
+ _bulletedList[indent] ??= [];
+ _bulletedList[indent]?.add(textNode);
+ } else {
+ _bulletedList.clear();
+ _bulletedList[0] ??= [];
+ _bulletedList[0]?.add(textNode);
+ }
+ break;
+ case 'ordered':
+ textNode.updateAttributes({
+ BuiltInAttributeKey.subtype: BuiltInAttributeKey.numberList,
+ BuiltInAttributeKey.number: _number++,
+ });
+ break;
+ case 'checked':
+ textNode.updateAttributes({
+ BuiltInAttributeKey.subtype: BuiltInAttributeKey.checkbox,
+ BuiltInAttributeKey.checkbox: true,
+ });
+ break;
+ case 'unchecked':
+ textNode.updateAttributes({
+ BuiltInAttributeKey.subtype: BuiltInAttributeKey.checkbox,
+ BuiltInAttributeKey.checkbox: false,
+ });
+ break;
+ }
+ }
+ }
+
+ bool _containNumberListStyle(Map? attributes) {
+ final list = attributes?['list'] as String?;
+ return list == 'ordered';
+ }
+
+ bool _containIndentBulletedListStyle(Map? attributes) {
+ final list = attributes?['list'] as String?;
+ final indent = attributes?['indent'] as int?;
+ return list == 'bullet' && indent != null;
+ }
+
+ int _indentLevel(Map? attributes) {
+ final indent = attributes?['indent'] as int?;
+ return indent ?? 1;
+ }
+
+ // convert header to appflowy style heading
+ void _applyHeaderStyle(TextNode textNode, Map? attributes) {
+ final header = attributes?['header'] as int?;
+ if (header != null) {
+ textNode.updateAttributes({
+ BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading,
+ BuiltInAttributeKey.heading: 'h$header',
+ });
+ }
+ }
+
+ // convert indent to tab
+ void _applyIndent(TextNode textNode, Map? attributes) {
+ final indent = attributes?['indent'] as int?;
+ final list = attributes?['list'] as String?;
+ if (indent != null && list == null) {
+ textNode.delta = textNode.delta.compose(
+ Delta()
+ ..retain(0)
+ ..insert(' ' * indent),
+ );
+ }
+ }
+
+ /*
+ // convert code-block to appflowy style code
+ void _applyCodeBlock(TextNode textNode, Map? attributes) {
+ final codeBlock = attributes?['code-block'] as bool?;
+ if (codeBlock != null) {
+ textNode.updateAttributes({
+ BuiltInAttributeKey.subtype: 'code_block',
+ });
+ }
+ }
+ */
+
+ void _applyBlockquote(TextNode textNode, Map? attributes) {
+ final blockquote = attributes?['blockquote'] as bool?;
+ if (blockquote != null) {
+ textNode.updateAttributes({
+ BuiltInAttributeKey.subtype: BuiltInAttributeKey.quote,
+ });
+ }
+ }
+}
+
+extension on Color {
+ String toHex() {
+ return '0x${value.toRadixString(16)}';
+ }
+}
diff --git a/frontend/app_flowy/packages/appflowy_editor/test/plugins/quill_delta/delta_document_encoder_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/plugins/quill_delta/delta_document_encoder_test.dart
new file mode 100644
index 0000000000..1fa4b3d95d
--- /dev/null
+++ b/frontend/app_flowy/packages/appflowy_editor/test/plugins/quill_delta/delta_document_encoder_test.dart
@@ -0,0 +1,551 @@
+import 'dart:convert';
+
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() async {
+ group('delta_document_encoder.dart', () {
+ test('', () {
+ final json = jsonDecode(quillDeltaSample.replaceAll('\\\\\n', '\\n'));
+ final document = DeltaDocumentConvert().convertFromJSON(json);
+ expect(jsonEncode(document.toJson()), documentSample);
+ });
+ });
+}
+
+const documentSample =
+ r'''{"document":{"type":"editor","children":[{"type":"text","attributes":{"subtype":"heading","heading":"h1"},"delta":[{"insert":"Flutter Quill"}]},{"type":"text","delta":[]},{"type":"text","attributes":{"subtype":"heading","heading":"h2"},"delta":[{"insert":"Rich text editor for Flutter"}]},{"type":"text","attributes":{"subtype":"heading","heading":"h3"},"delta":[{"insert":"Quill component for Flutter"}]},{"type":"text","delta":[{"insert":"This "},{"insert":"library","attributes":{"italic":true}},{"insert":" supports "},{"insert":"mobile","attributes":{"bold":true,"backgroundColor":"0xFFebd6ff"}},{"insert":" platform "},{"insert":"only","attributes":{"underline":true,"bold":true,"color":"0xFFe60000"}},{"insert":" and ","attributes":{"color":"0xd7000000"}},{"insert":"web","attributes":{"strikethrough":true}},{"insert":" is not supported."}]},{"type":"text","delta":[{"insert":"You are welcome to use "},{"insert":"Bullet Journal","attributes":{"href":"https://bulletjournal.us/home/index.html"}},{"insert":":"}]},{"type":"text","attributes":{"subtype":"number-list","number":1},"delta":[{"insert":"Track personal and group journals (ToDo, Note, Ledger) from multiple views with timely reminders"}]},{"type":"text","attributes":{"subtype":"number-list","number":2},"delta":[{"insert":"Share your tasks and notes with teammates, and see changes as they happen in real-time, across all devices"}]},{"type":"text","attributes":{"subtype":"number-list","number":3},"delta":[{"insert":"Check out what you and your teammates are working on each day"}]},{"type":"text","delta":[]},{"type":"text","attributes":{"subtype":"bulleted-list"},"delta":[{"insert":"Splitting bills with friends can never be easier."}]},{"type":"text","attributes":{"subtype":"bulleted-list"},"delta":[{"insert":"Start creating a group and invite your friends to join."}]},{"type":"text","attributes":{"subtype":"bulleted-list"},"delta":[{"insert":"Create a BuJo of Ledger type to see expense or balance summary."}]},{"type":"text","delta":[]},{"type":"text","attributes":{"subtype":"quote"},"delta":[{"insert":"Attach one or multiple labels to tasks, notes or transactions. Later you can track them just using the label(s)."}]},{"type":"text","delta":[]},{"type":"text","delta":[{"insert":"var BuJo = 'Bullet' + 'Journal'"}]},{"type":"text","delta":[]},{"type":"text","delta":[{"insert":" Start tracking in your browser"}]},{"type":"text","delta":[{"insert":" Stop the timer on your phone"}]},{"type":"text","delta":[{"insert":" All your time entries are synced"}]},{"type":"text","delta":[{"insert":" between the phone apps"}]},{"type":"text","delta":[{"insert":" and the website."}]},{"type":"text","delta":[]},{"type":"text","delta":[]},{"type":"text","delta":[{"insert":"Center Align"}]},{"type":"text","delta":[{"insert":"Right Align"}]},{"type":"text","delta":[{"insert":"Justify Align"}]},{"type":"text","attributes":{"subtype":"number-list","number":1},"delta":[{"insert":"Have trouble finding things? "}]},{"type":"text","attributes":{"subtype":"number-list","number":2},"delta":[{"insert":"Just type in the search bar"}]},{"type":"text","attributes":{"subtype":"number-list","number":3},"delta":[{"insert":"and easily find contents"}]},{"type":"text","attributes":{"subtype":"number-list","number":4},"delta":[{"insert":"across projects or folders."}]},{"type":"text","attributes":{"subtype":"number-list","number":5},"delta":[{"insert":"It matches text in your note or task."}]},{"type":"text","attributes":{"subtype":"number-list","number":6},"delta":[{"insert":"Enable reminders so that you will get notified by"}]},{"type":"text","attributes":{"subtype":"number-list","number":7},"delta":[{"insert":"email"}]},{"type":"text","attributes":{"subtype":"number-list","number":8},"delta":[{"insert":"message on your phone"}]},{"type":"text","attributes":{"subtype":"number-list","number":9},"delta":[{"insert":"popup on the web site"}]},{"type":"text","children":[{"type":"text","children":[{"type":"text","attributes":{"subtype":"bulleted-list"},"delta":[{"insert":"tasks"}]},{"type":"text","attributes":{"subtype":"bulleted-list"},"delta":[{"insert":"notes"}]},{"type":"text","children":[{"type":"text","attributes":{"subtype":"bulleted-list"},"delta":[{"insert":"under BuJo "}]}],"attributes":{"subtype":"bulleted-list"},"delta":[{"insert":"transactions"}]}],"attributes":{"subtype":"bulleted-list"},"delta":[{"insert":"Organize your"}]}],"attributes":{"subtype":"bulleted-list"},"delta":[{"insert":"Create a BuJo serving as project or folder"}]},{"type":"text","children":[{"type":"text","attributes":{"subtype":"bulleted-list"},"delta":[{"insert":"or hierarchical view"}]}],"attributes":{"subtype":"bulleted-list"},"delta":[{"insert":"See them in Calendar"}]},{"type":"text","attributes":{"subtype":"checkbox","checkbox":true},"delta":[{"insert":"this is a check list"}]},{"type":"text","attributes":{"subtype":"checkbox","checkbox":false},"delta":[{"insert":"this is a uncheck list"}]},{"type":"text","delta":[{"insert":"Font Sans Serif Serif Monospace Size Small Large Hugefont size 15 font size 35 font size 20 diff-match-patch"}]},{"type":"text","delta":[{"insert":""}]}]}}''';
+
+const quillDeltaSample = r'''
+[
+ {
+ "insert": "Flutter Quill"
+ },
+ {
+ "attributes": {
+ "header": 1
+ },
+ "insert": "\n"
+ },
+ {
+ "insert": {
+ "video": "https://www.youtube.com/watch?v=V4hgdKhIqtc&list=PLbhaS_83B97s78HsDTtplRTEhcFsqSqIK&index=1"
+ }
+ },
+ {
+ "insert": {
+ "video": "https://user-images.githubusercontent.com/122956/126238875-22e42501-ad41-4266-b1d6-3f89b5e3b79b.mp4"
+ }
+ },
+ {
+ "insert": "\nRich text editor for Flutter"
+ },
+ {
+ "attributes": {
+ "header": 2
+ },
+ "insert": "\n"
+ },
+ {
+ "insert": "Quill component for Flutter"
+ },
+ {
+ "attributes": {
+ "header": 3
+ },
+ "insert": "\n"
+ },
+ {
+ "insert": "This "
+ },
+ {
+ "attributes": {
+ "italic": true,
+ "background": "transparent"
+ },
+ "insert": "library"
+ },
+ {
+ "insert": " supports "
+ },
+ {
+ "attributes": {
+ "bold": true,
+ "background": "#ebd6ff"
+ },
+ "insert": "mobile"
+ },
+ {
+ "insert": " platform "
+ },
+ {
+ "attributes": {
+ "underline": true,
+ "bold": true,
+ "color": "#e60000"
+ },
+ "insert": "only"
+ },
+ {
+ "attributes": {
+ "color": "rgba(0, 0, 0, 0.847)"
+ },
+ "insert": " and "
+ },
+ {
+ "attributes": {
+ "strike": true,
+ "color": "black"
+ },
+ "insert": "web"
+ },
+ {
+ "insert": " is not supported.\nYou are welcome to use "
+ },
+ {
+ "attributes": {
+ "link": "https://bulletjournal.us/home/index.html"
+ },
+ "insert": "Bullet Journal"
+ },
+ {
+ "insert": ":\nTrack personal and group journals (ToDo, Note, Ledger) from multiple views with timely reminders"
+ },
+ {
+ "attributes": {
+ "list": "ordered"
+ },
+ "insert": "\n"
+ },
+ {
+ "insert": "Share your tasks and notes with teammates, and see changes as they happen in real-time, across all devices"
+ },
+ {
+ "attributes": {
+ "list": "ordered"
+ },
+ "insert": "\n"
+ },
+ {
+ "insert": "Check out what you and your teammates are working on each day"
+ },
+ {
+ "attributes": {
+ "list": "ordered"
+ },
+ "insert": "\n"
+ },
+ {
+ "insert": "\nSplitting bills with friends can never be easier."
+ },
+ {
+ "attributes": {
+ "list": "bullet"
+ },
+ "insert": "\n"
+ },
+ {
+ "insert": "Start creating a group and invite your friends to join."
+ },
+ {
+ "attributes": {
+ "list": "bullet"
+ },
+ "insert": "\n"
+ },
+ {
+ "insert": "Create a BuJo of Ledger type to see expense or balance summary."
+ },
+ {
+ "attributes": {
+ "list": "bullet"
+ },
+ "insert": "\n"
+ },
+ {
+ "insert": "\nAttach one or multiple labels to tasks, notes or transactions. Later you can track them just using the label(s)."
+ },
+ {
+ "attributes": {
+ "blockquote": true
+ },
+ "insert": "\n"
+ },
+ {
+ "insert": "\nvar BuJo = 'Bullet' + 'Journal'"
+ },
+ {
+ "attributes": {
+ "code-block": true
+ },
+ "insert": "\n"
+ },
+ {
+ "insert": "\nStart tracking in your browser"
+ },
+ {
+ "attributes": {
+ "indent": 1
+ },
+ "insert": "\n"
+ },
+ {
+ "insert": "Stop the timer on your phone"
+ },
+ {
+ "attributes": {
+ "indent": 1
+ },
+ "insert": "\n"
+ },
+ {
+ "insert": "All your time entries are synced"
+ },
+ {
+ "attributes": {
+ "indent": 2
+ },
+ "insert": "\n"
+ },
+ {
+ "insert": "between the phone apps"
+ },
+ {
+ "attributes": {
+ "indent": 2
+ },
+ "insert": "\n"
+ },
+ {
+ "insert": "and the website."
+ },
+ {
+ "attributes": {
+ "indent": 3
+ },
+ "insert": "\n"
+ },
+ {
+ "insert": "\n"
+ },
+ {
+ "insert": "\nCenter Align"
+ },
+ {
+ "attributes": {
+ "align": "center"
+ },
+ "insert": "\n"
+ },
+ {
+ "insert": "Right Align"
+ },
+ {
+ "attributes": {
+ "align": "right"
+ },
+ "insert": "\n"
+ },
+ {
+ "insert": "Justify Align"
+ },
+ {
+ "attributes": {
+ "align": "justify"
+ },
+ "insert": "\n"
+ },
+ {
+ "insert": "Have trouble finding things? "
+ },
+ {
+ "attributes": {
+ "list": "ordered"
+ },
+ "insert": "\n"
+ },
+ {
+ "insert": "Just type in the search bar"
+ },
+ {
+ "attributes": {
+ "indent": 1,
+ "list": "ordered"
+ },
+ "insert": "\n"
+ },
+ {
+ "insert": "and easily find contents"
+ },
+ {
+ "attributes": {
+ "indent": 2,
+ "list": "ordered"
+ },
+ "insert": "\n"
+ },
+ {
+ "insert": "across projects or folders."
+ },
+ {
+ "attributes": {
+ "indent": 2,
+ "list": "ordered"
+ },
+ "insert": "\n"
+ },
+ {
+ "insert": "It matches text in your note or task."
+ },
+ {
+ "attributes": {
+ "indent": 1,
+ "list": "ordered"
+ },
+ "insert": "\n"
+ },
+ {
+ "insert": "Enable reminders so that you will get notified by"
+ },
+ {
+ "attributes": {
+ "list": "ordered"
+ },
+ "insert": "\n"
+ },
+ {
+ "insert": "email"
+ },
+ {
+ "attributes": {
+ "indent": 1,
+ "list": "ordered"
+ },
+ "insert": "\n"
+ },
+ {
+ "insert": "message on your phone"
+ },
+ {
+ "attributes": {
+ "indent": 1,
+ "list": "ordered"
+ },
+ "insert": "\n"
+ },
+ {
+ "insert": "popup on the web site"
+ },
+ {
+ "attributes": {
+ "indent": 1,
+ "list": "ordered"
+ },
+ "insert": "\n"
+ },
+ {
+ "insert": "Create a BuJo serving as project or folder"
+ },
+ {
+ "attributes": {
+ "list": "bullet"
+ },
+ "insert": "\n"
+ },
+ {
+ "insert": "Organize your"
+ },
+ {
+ "attributes": {
+ "indent": 1,
+ "list": "bullet"
+ },
+ "insert": "\n"
+ },
+ {
+ "insert": "tasks"
+ },
+ {
+ "attributes": {
+ "indent": 2,
+ "list": "bullet"
+ },
+ "insert": "\n"
+ },
+ {
+ "insert": "notes"
+ },
+ {
+ "attributes": {
+ "indent": 2,
+ "list": "bullet"
+ },
+ "insert": "\n"
+ },
+ {
+ "insert": "transactions"
+ },
+ {
+ "attributes": {
+ "indent": 2,
+ "list": "bullet"
+ },
+ "insert": "\n"
+ },
+ {
+ "insert": "under BuJo "
+ },
+ {
+ "attributes": {
+ "indent": 3,
+ "list": "bullet"
+ },
+ "insert": "\n"
+ },
+ {
+ "insert": "See them in Calendar"
+ },
+ {
+ "attributes": {
+ "list": "bullet"
+ },
+ "insert": "\n"
+ },
+ {
+ "insert": "or hierarchical view"
+ },
+ {
+ "attributes": {
+ "indent": 1,
+ "list": "bullet"
+ },
+ "insert": "\n"
+ },
+ {
+ "insert": "this is a check list"
+ },
+ {
+ "attributes": {
+ "list": "checked"
+ },
+ "insert": "\n"
+ },
+ {
+ "insert": "this is a uncheck list"
+ },
+ {
+ "attributes": {
+ "list": "unchecked"
+ },
+ "insert": "\n"
+ },
+ {
+ "insert": "Font "
+ },
+ {
+ "attributes": {
+ "font": "sans-serif"
+ },
+ "insert": "Sans Serif"
+ },
+ {
+ "insert": " "
+ },
+ {
+ "attributes": {
+ "font": "serif"
+ },
+ "insert": "Serif"
+ },
+ {
+ "insert": " "
+ },
+ {
+ "attributes": {
+ "font": "monospace"
+ },
+ "insert": "Monospace"
+ },
+ {
+ "insert": " Size "
+ },
+ {
+ "attributes": {
+ "size": "small"
+ },
+ "insert": "Small"
+ },
+ {
+ "insert": " "
+ },
+ {
+ "attributes": {
+ "size": "large"
+ },
+ "insert": "Large"
+ },
+ {
+ "insert": " "
+ },
+ {
+ "attributes": {
+ "size": "huge"
+ },
+ "insert": "Huge"
+ },
+ {
+ "attributes": {
+ "size": "15.0"
+ },
+ "insert": "font size 15"
+ },
+ {
+ "insert": " "
+ },
+ {
+ "attributes": {
+ "size": "35"
+ },
+ "insert": "font size 35"
+ },
+ {
+ "insert": " "
+ },
+ {
+ "attributes": {
+ "size": "20"
+ },
+ "insert": "font size 20"
+ },
+ {
+ "attributes": {
+ "token": "built_in"
+ },
+ "insert": " diff"
+ },
+ {
+ "attributes": {
+ "token": "operator"
+ },
+ "insert": "-match"
+ },
+ {
+ "attributes": {
+ "token": "literal"
+ },
+ "insert": "-patch"
+ },
+ {
+ "insert": {
+ "image": "https://user-images.githubusercontent.com/122956/72955931-ccc07900-3d52-11ea-89b1-d468a6e2aa2b.png"
+ },
+ "attributes": {
+ "width": "230",
+ "style": "display: block; margin: auto;"
+ }
+ },
+ {
+ "insert": "\n"
+ }
+]
+''';