|
| 1 | +import 'package:appflowy_editor/src/core/document/attributes.dart'; |
| 2 | +import 'package:appflowy_editor/src/core/document/document.dart'; |
| 3 | +import 'package:appflowy_editor/src/core/document/node.dart'; |
| 4 | +import 'package:appflowy_editor/src/core/document/text_delta.dart'; |
| 5 | +import 'package:appflowy_editor/src/core/legacy/built_in_attribute_keys.dart'; |
| 6 | +import 'package:flutter/material.dart'; |
| 7 | + |
| 8 | +class DeltaDocumentConvert { |
| 9 | + DeltaDocumentConvert(); |
| 10 | + |
| 11 | + var _number = 1; |
| 12 | + final Map<int, List<TextNode>> _bulletedList = {}; |
| 13 | + |
| 14 | + Document convertFromJSON(List<dynamic> json) { |
| 15 | + final delta = Delta.fromJson(json); |
| 16 | + return convertFromDelta(delta); |
| 17 | + } |
| 18 | + |
| 19 | + Document convertFromDelta(Delta delta) { |
| 20 | + final iter = delta.iterator; |
| 21 | + |
| 22 | + final document = Document.empty(); |
| 23 | + TextNode textNode = TextNode(delta: Delta()); |
| 24 | + int path = 0; |
| 25 | + |
| 26 | + while (iter.moveNext()) { |
| 27 | + final op = iter.current; |
| 28 | + if (op is TextInsert) { |
| 29 | + if (op.text != '\n') { |
| 30 | + // Attributes associated with a newline character describes formatting for that line. |
| 31 | + final texts = op.text.split('\n'); |
| 32 | + if (texts.length > 1) { |
| 33 | + textNode.delta.insert(texts[0]); |
| 34 | + document.insert([path++], [textNode]); |
| 35 | + textNode = TextNode(delta: Delta()..insert(texts[1])); |
| 36 | + } else { |
| 37 | + _applyStyle(textNode, op.text, op.attributes); |
| 38 | + } |
| 39 | + } else { |
| 40 | + if (!_containNumberListStyle(op.attributes)) { |
| 41 | + _number = 1; |
| 42 | + } |
| 43 | + _applyListStyle(textNode, op.attributes); |
| 44 | + _applyHeaderStyle(textNode, op.attributes); |
| 45 | + _applyIndent(textNode, op.attributes); |
| 46 | + _applyBlockquote(textNode, op.attributes); |
| 47 | + // _applyCodeBlock(textNode, op.attributes); |
| 48 | + |
| 49 | + if (_containIndentBulletedListStyle(op.attributes)) { |
| 50 | + final level = _indentLevel(op.attributes); |
| 51 | + final path = [ |
| 52 | + ..._bulletedList[level - 1]!.last.path, |
| 53 | + _bulletedList[level]!.length - 1, |
| 54 | + ]; |
| 55 | + document.insert(path, [textNode]); |
| 56 | + } else { |
| 57 | + document.insert([path++], [textNode]); |
| 58 | + } |
| 59 | + textNode = TextNode(delta: Delta()); |
| 60 | + } |
| 61 | + } else { |
| 62 | + assert(false, 'op must be TextInsert'); |
| 63 | + } |
| 64 | + } |
| 65 | + |
| 66 | + return document; |
| 67 | + } |
| 68 | + |
| 69 | + void _applyStyle(TextNode textNode, String text, Map? attributes) { |
| 70 | + Attributes attrs = {}; |
| 71 | + |
| 72 | + if (_containsStyle(attributes, 'strike')) { |
| 73 | + attrs[BuiltInAttributeKey.strikethrough] = true; |
| 74 | + } |
| 75 | + if (_containsStyle(attributes, 'underline')) { |
| 76 | + attrs[BuiltInAttributeKey.underline] = true; |
| 77 | + } |
| 78 | + if (_containsStyle(attributes, 'bold')) { |
| 79 | + attrs[BuiltInAttributeKey.bold] = true; |
| 80 | + } |
| 81 | + if (_containsStyle(attributes, 'italic')) { |
| 82 | + attrs[BuiltInAttributeKey.italic] = true; |
| 83 | + } |
| 84 | + final link = attributes?['link'] as String?; |
| 85 | + if (link != null) { |
| 86 | + attrs[BuiltInAttributeKey.href] = link; |
| 87 | + } |
| 88 | + final color = attributes?['color'] as String?; |
| 89 | + final colorHex = _convertColorToHexString(color); |
| 90 | + if (colorHex != null) { |
| 91 | + attrs[BuiltInAttributeKey.color] = colorHex; |
| 92 | + } |
| 93 | + final backgroundColor = attributes?['background'] as String?; |
| 94 | + final backgroundHex = _convertColorToHexString(backgroundColor); |
| 95 | + if (backgroundHex != null) { |
| 96 | + attrs[BuiltInAttributeKey.backgroundColor] = backgroundHex; |
| 97 | + } |
| 98 | + |
| 99 | + textNode.delta.insert(text, attributes: attrs); |
| 100 | + } |
| 101 | + |
| 102 | + bool _containsStyle(Map? attributes, String key) { |
| 103 | + final value = attributes?[key] as bool?; |
| 104 | + return value == true; |
| 105 | + } |
| 106 | + |
| 107 | + String? _convertColorToHexString(String? color) { |
| 108 | + if (color == null) { |
| 109 | + return null; |
| 110 | + } |
| 111 | + if (color.startsWith('#')) { |
| 112 | + return '0xFF${color.substring(1)}'; |
| 113 | + } else if (color.startsWith("rgba")) { |
| 114 | + List rgbaList = color.substring(5, color.length - 1).split(','); |
| 115 | + return Color.fromRGBO( |
| 116 | + int.parse(rgbaList[0]), |
| 117 | + int.parse(rgbaList[1]), |
| 118 | + int.parse(rgbaList[2]), |
| 119 | + double.parse(rgbaList[3]), |
| 120 | + ).toHex(); |
| 121 | + } |
| 122 | + return null; |
| 123 | + } |
| 124 | + |
| 125 | + // convert bullet-list, number-list, check-list to appflowy style list. |
| 126 | + void _applyListStyle(TextNode textNode, Map? attributes) { |
| 127 | + final indent = attributes?['indent'] as int?; |
| 128 | + final list = attributes?['list'] as String?; |
| 129 | + if (list != null) { |
| 130 | + switch (list) { |
| 131 | + case 'bullet': |
| 132 | + textNode.updateAttributes({ |
| 133 | + BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList, |
| 134 | + }); |
| 135 | + if (indent != null) { |
| 136 | + _bulletedList[indent] ??= []; |
| 137 | + _bulletedList[indent]?.add(textNode); |
| 138 | + } else { |
| 139 | + _bulletedList.clear(); |
| 140 | + _bulletedList[0] ??= []; |
| 141 | + _bulletedList[0]?.add(textNode); |
| 142 | + } |
| 143 | + break; |
| 144 | + case 'ordered': |
| 145 | + textNode.updateAttributes({ |
| 146 | + BuiltInAttributeKey.subtype: BuiltInAttributeKey.numberList, |
| 147 | + BuiltInAttributeKey.number: _number++, |
| 148 | + }); |
| 149 | + break; |
| 150 | + case 'checked': |
| 151 | + textNode.updateAttributes({ |
| 152 | + BuiltInAttributeKey.subtype: BuiltInAttributeKey.checkbox, |
| 153 | + BuiltInAttributeKey.checkbox: true, |
| 154 | + }); |
| 155 | + break; |
| 156 | + case 'unchecked': |
| 157 | + textNode.updateAttributes({ |
| 158 | + BuiltInAttributeKey.subtype: BuiltInAttributeKey.checkbox, |
| 159 | + BuiltInAttributeKey.checkbox: false, |
| 160 | + }); |
| 161 | + break; |
| 162 | + } |
| 163 | + } |
| 164 | + } |
| 165 | + |
| 166 | + bool _containNumberListStyle(Map? attributes) { |
| 167 | + final list = attributes?['list'] as String?; |
| 168 | + return list == 'ordered'; |
| 169 | + } |
| 170 | + |
| 171 | + bool _containIndentBulletedListStyle(Map? attributes) { |
| 172 | + final list = attributes?['list'] as String?; |
| 173 | + final indent = attributes?['indent'] as int?; |
| 174 | + return list == 'bullet' && indent != null; |
| 175 | + } |
| 176 | + |
| 177 | + int _indentLevel(Map? attributes) { |
| 178 | + final indent = attributes?['indent'] as int?; |
| 179 | + return indent ?? 1; |
| 180 | + } |
| 181 | + |
| 182 | + // convert header to appflowy style heading |
| 183 | + void _applyHeaderStyle(TextNode textNode, Map? attributes) { |
| 184 | + final header = attributes?['header'] as int?; |
| 185 | + if (header != null) { |
| 186 | + textNode.updateAttributes({ |
| 187 | + BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading, |
| 188 | + BuiltInAttributeKey.heading: 'h$header', |
| 189 | + }); |
| 190 | + } |
| 191 | + } |
| 192 | + |
| 193 | + // convert indent to tab |
| 194 | + void _applyIndent(TextNode textNode, Map? attributes) { |
| 195 | + final indent = attributes?['indent'] as int?; |
| 196 | + final list = attributes?['list'] as String?; |
| 197 | + if (indent != null && list == null) { |
| 198 | + textNode.delta = textNode.delta.compose( |
| 199 | + Delta() |
| 200 | + ..retain(0) |
| 201 | + ..insert(' ' * indent), |
| 202 | + ); |
| 203 | + } |
| 204 | + } |
| 205 | + |
| 206 | + /* |
| 207 | + // convert code-block to appflowy style code |
| 208 | + void _applyCodeBlock(TextNode textNode, Map? attributes) { |
| 209 | + final codeBlock = attributes?['code-block'] as bool?; |
| 210 | + if (codeBlock != null) { |
| 211 | + textNode.updateAttributes({ |
| 212 | + BuiltInAttributeKey.subtype: 'code_block', |
| 213 | + }); |
| 214 | + } |
| 215 | + } |
| 216 | + */ |
| 217 | + |
| 218 | + void _applyBlockquote(TextNode textNode, Map? attributes) { |
| 219 | + final blockquote = attributes?['blockquote'] as bool?; |
| 220 | + if (blockquote != null) { |
| 221 | + textNode.updateAttributes({ |
| 222 | + BuiltInAttributeKey.subtype: BuiltInAttributeKey.quote, |
| 223 | + }); |
| 224 | + } |
| 225 | + } |
| 226 | +} |
| 227 | + |
| 228 | +extension on Color { |
| 229 | + String toHex() { |
| 230 | + return '0x${value.toRadixString(16)}'; |
| 231 | + } |
| 232 | +} |
0 commit comments