Created
May 16, 2020 12:19
-
-
Save Faiyyaz/452095d737b5f7686b1e036d81e1f9f2 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file | |
// for details. All rights reserved. Use of this source code is governed by a | |
// BSD-style license that can be found in the LICENSE file. | |
import 'dart:collection'; | |
import 'dart:convert'; | |
import 'package:html/dom.dart'; | |
import 'package:html/parser.dart' show parse; | |
import 'package:notus/notus.dart'; | |
import 'package:quill_delta/quill_delta.dart'; | |
class NotusHTMLCodec extends Codec<Delta, String> { | |
const NotusHTMLCodec(); | |
@override | |
Converter<String, Delta> get decoder => _HTMLNotusDecoder(); | |
@override | |
Converter<Delta, String> get encoder => _NotusHTMLEncoder(); | |
} | |
class Keys { | |
static const line = "line"; | |
static const inline = "inline"; | |
} | |
class DeltaKeys { | |
static const ol = "ol"; | |
static const ul = "ul"; | |
static const a = "a"; | |
static const i = "i"; | |
static const b = "b"; | |
static const quote = "quote"; | |
static const code = "code"; | |
static const type = "type"; | |
static const block = "block"; | |
static const image = "image"; | |
static const imageSrc = "source"; | |
static const hr = "hr"; | |
static const insert = "insert"; | |
static const attributes = "attributes"; | |
static const heading = "heading"; | |
static const embed = "embed"; | |
} | |
class HtmlKeys { | |
static const blockquote = "blockquote"; | |
static const unorderedList = "ul"; | |
static const orderedList = "ol"; | |
static const list = "li"; | |
static const heading = "h"; | |
static const h1 = "h1"; | |
static const h2 = "h2"; | |
static const h3 = "h3"; | |
static const anchor = "a"; | |
static const anchorHref = "href"; | |
static const bold = "b"; | |
static const italic = "i"; | |
static const horizontalRule = "hr"; | |
static const image = "img"; | |
static const imageSrc = "src"; | |
static const br = "br"; | |
static const preformatted = "pre"; | |
} | |
String htmlTagNameToDeltaAttributeName(String htmlTag) { | |
switch (htmlTag) { | |
case HtmlKeys.blockquote: | |
case HtmlKeys.unorderedList: | |
case HtmlKeys.orderedList: | |
case HtmlKeys.preformatted: | |
return DeltaKeys.block; | |
case HtmlKeys.h1: | |
case HtmlKeys.h2: | |
case HtmlKeys.h3: | |
return DeltaKeys.heading; | |
case HtmlKeys.anchor: | |
return DeltaKeys.a; | |
case HtmlKeys.bold: | |
return DeltaKeys.b; | |
case HtmlKeys.italic: | |
return DeltaKeys.i; | |
case HtmlKeys.horizontalRule: | |
case HtmlKeys.image: | |
return DeltaKeys.embed; | |
case HtmlKeys.br: | |
case HtmlKeys.list: | |
default: | |
return null; | |
} | |
} | |
class _NotusHTMLEncoder extends Converter<Delta, String> { | |
static final kSimpleBlocks = <NotusAttribute, String>{ | |
NotusAttribute.bq: HtmlKeys.blockquote, | |
NotusAttribute.ul: HtmlKeys.unorderedList, | |
NotusAttribute.ol: HtmlKeys.orderedList, | |
}; | |
Map<String, dynamic> container; | |
String buildContainer(String key) { | |
if (container == null || !container.containsKey(key)) { | |
return ''; | |
} | |
final attributes = Map<String, dynamic>.from(container[key]); | |
final buffer = StringBuffer(); | |
final length = attributes.length; | |
int counter = 0; | |
buffer.write(" "); | |
attributes.forEach((key, val) { | |
if (val == null) { | |
buffer.write(key); | |
} else { | |
buffer.write("$key=\"$val\""); | |
} | |
counter++; | |
if (counter < length) { | |
buffer.write(" "); | |
} | |
}); | |
return buffer.toString(); | |
} | |
@override | |
String convert(Delta input) { | |
final iterator = DeltaIterator(input); | |
final buffer = StringBuffer(); | |
final lineBuffer = StringBuffer(); | |
final currentBlockLines = <String>[]; | |
NotusAttribute<String> currentBlockStyle; | |
NotusStyle currentInlineStyle = NotusStyle(); | |
void _handleBlock(NotusAttribute<String> blockStyle) { | |
if (currentBlockLines.isEmpty) { | |
return; // Empty block | |
} | |
if (blockStyle == null) { | |
buffer.write(currentBlockLines.join('\n')); | |
} else if (blockStyle == NotusAttribute.bq || | |
blockStyle == NotusAttribute.code) { | |
_writeBlockTag(buffer, blockStyle, start: true); | |
buffer.write(currentBlockLines.join("\n")); | |
_writeBlockTag(buffer, blockStyle, close: true); | |
} else { | |
for (var i = 0; i < currentBlockLines.length; i++) { | |
final line = currentBlockLines[i]; | |
if (i == 0) { | |
_writeBlockTag(buffer, blockStyle, start: true); | |
buffer.writeln(); | |
} | |
buffer.write("<${HtmlKeys.list}>"); | |
buffer.write(line); | |
buffer.write("</${HtmlKeys.list}>"); | |
buffer.writeln(); | |
if (i == currentBlockLines.length - 1) { | |
_writeBlockTag(buffer, blockStyle, close: true); | |
} | |
} | |
} | |
buffer.writeln(); | |
} | |
void _handleSpan(String text, Map<String, dynamic> attributes) { | |
final style = NotusStyle.fromJson(attributes); | |
currentInlineStyle = | |
_writeInline(lineBuffer, text, style, currentInlineStyle); | |
} | |
void _handleLine(Map<String, dynamic> attributes) { | |
final style = NotusStyle.fromJson(attributes); | |
final lineBlock = style.get(NotusAttribute.block); | |
if (lineBlock == currentBlockStyle) { | |
currentBlockLines.add(_writeLine(lineBuffer.toString(), style)); | |
} else { | |
_handleBlock(currentBlockStyle); | |
currentBlockLines.clear(); | |
currentBlockLines.add(_writeLine(lineBuffer.toString(), style)); | |
currentBlockStyle = lineBlock; | |
} | |
lineBuffer.clear(); | |
} | |
String removeZeroWidthSpace(String text) { | |
return text.replaceAll(String.fromCharCode(8203), ""); | |
} | |
while (iterator.hasNext) { | |
final op = iterator.next(); | |
final lf = op.data.indexOf('\n'); | |
if (lf == -1) { | |
_handleSpan(removeZeroWidthSpace(op.data), op.attributes); | |
} else { | |
final span = StringBuffer(); | |
for (var i = 0; i < op.data.length; i++) { | |
if (op.data.codeUnitAt(i) == 0x0A) { | |
if (span.isNotEmpty) { | |
// Write the span if it's not empty. | |
_handleSpan(span.toString() + '<br>', op.attributes); | |
} | |
// Close any open inline styles. | |
_handleSpan('<br>', null); | |
_handleLine(op.attributes); | |
span.clear(); | |
} else { | |
span.writeCharCode(op.data.codeUnitAt(i)); | |
} | |
} | |
// Remaining span | |
if (span.isNotEmpty) { | |
_handleSpan(removeZeroWidthSpace(span.toString()), op.attributes); | |
} | |
} | |
// container = null; | |
} | |
_handleBlock(currentBlockStyle); // Close the last block | |
return buffer.toString(); | |
} | |
String _writeLine(String text, NotusStyle style) { | |
final buffer = StringBuffer(); | |
if (style.contains(NotusAttribute.heading)) { | |
_writeAttribute(buffer, style.get(NotusAttribute.heading)); | |
} | |
// Write the text itself | |
buffer.write(text); | |
if (style.contains(NotusAttribute.heading)) { | |
_writeAttribute(buffer, style.get(NotusAttribute.heading), close: true); | |
} | |
return buffer.toString(); | |
} | |
String _trimRight(StringBuffer buffer) { | |
final text = buffer.toString(); | |
if (!text.endsWith(' ')) { | |
return ''; | |
} | |
final result = text.trimRight(); | |
buffer.clear(); | |
buffer.write(result); | |
return ' ' * (text.length - result.length); | |
} | |
NotusStyle _writeInline(StringBuffer buffer, String text, NotusStyle style, | |
NotusStyle currentStyle) { | |
// First close any current styles if needed | |
for (final value in currentStyle.values.toList().reversed) { | |
if (value.scope == NotusAttributeScope.line) { | |
continue; | |
} | |
if (style.containsSame(value)) { | |
continue; | |
} | |
final padding = _trimRight(buffer); | |
_writeAttribute(buffer, value, close: true); | |
if (padding.isNotEmpty) { | |
buffer.write(padding); | |
} | |
} | |
// Now open any new styles. | |
for (final value in style.values) { | |
if (value.scope == NotusAttributeScope.line) { | |
continue; | |
} | |
if (currentStyle.containsSame(value)) { | |
continue; | |
} | |
final originalText = text; | |
text = text.trimLeft(); | |
final padding = ' ' * (originalText.length - text.length); | |
if (padding.isNotEmpty) { | |
buffer.write(padding); | |
} | |
_writeAttribute(buffer, value); | |
} | |
// Write the text itself | |
buffer.write(text); | |
return style; | |
} | |
void _writeAttribute(StringBuffer buffer, NotusAttribute attribute, | |
{bool close = false}) { | |
if (attribute == NotusAttribute.bold) { | |
_writeBoldTag(buffer, close: close); | |
} else if (attribute == NotusAttribute.italic) { | |
_writeItalicTag(buffer, close: close); | |
} else if (attribute.key == NotusAttribute.link.key) { | |
_writeLinkTag(buffer, attribute, close: close); | |
} else if (attribute.key == NotusAttribute.heading.key) { | |
_writeHeadingTag(buffer, attribute, close: close); | |
} else if (attribute.key == NotusAttribute.block.key) { | |
_writeBlockTag(buffer, attribute, close: close); | |
} else if (attribute.key == NotusAttribute.embed.key) { | |
_writeEmbedTag(buffer, attribute, close: close); | |
} else { | |
throw ArgumentError('Cannot handle $attribute'); | |
} | |
} | |
void _writeEmbedTag( | |
StringBuffer buffer, NotusAttribute<Map<String, dynamic>> embed, | |
{bool close = false}) { | |
if (embed.value[DeltaKeys.type] == DeltaKeys.image) { | |
if (close) { | |
return; | |
} | |
buffer.write( | |
"<${HtmlKeys.image} ${HtmlKeys.imageSrc}=\"${embed.value["source"]}\"${buildContainer(DeltaKeys.embed)} />"); | |
} else if (embed.value[DeltaKeys.type] == DeltaKeys.hr) { | |
if (close) { | |
return; | |
} | |
buffer.write( | |
"<${HtmlKeys.horizontalRule}${buildContainer(DeltaKeys.embed)} />"); | |
} | |
} | |
void _writeBoldTag(StringBuffer buffer, {bool close = false}) { | |
if (close) { | |
buffer.write('</${HtmlKeys.bold}>'); | |
} else { | |
buffer.write("<${HtmlKeys.bold}${buildContainer(DeltaKeys.b)}>"); | |
} | |
} | |
void _writeItalicTag(StringBuffer buffer, {bool close = false}) { | |
if (close) { | |
buffer.write('</${HtmlKeys.italic}>'); | |
} else { | |
buffer.write("<${HtmlKeys.italic}${buildContainer(DeltaKeys.i)}>"); | |
} | |
} | |
void _writeLinkTag(StringBuffer buffer, NotusAttribute<String> link, | |
{bool close = false}) { | |
if (close) { | |
buffer.write('</${HtmlKeys.anchor}>'); | |
} else { | |
buffer.write( | |
'<${HtmlKeys.anchor} ${HtmlKeys.anchorHref}=\"${link.value}\"${buildContainer(DeltaKeys.a)}>'); | |
} | |
} | |
void _writeHeadingTag(StringBuffer buffer, NotusAttribute<int> heading, | |
{bool close = false}) { | |
final level = heading.value; | |
if (close) { | |
buffer.write('</${HtmlKeys.heading}$level>'); | |
} else { | |
buffer.write( | |
'<${HtmlKeys.heading}$level${buildContainer(DeltaKeys.heading)}>'); | |
} | |
} | |
void _writeBlockTag(StringBuffer buffer, NotusAttribute<String> block, | |
{bool close = false, bool start = false}) { | |
if (block == NotusAttribute.code) { | |
if (start) { | |
buffer.write('<${HtmlKeys.preformatted}${buildContainer(block.key)}>'); | |
} else if (close) { | |
buffer.write('</${HtmlKeys.preformatted}>'); | |
} | |
} else { | |
final tag = kSimpleBlocks[block]; | |
if (start) { | |
buffer.write("<${tag}${buildContainer(block.key)}>"); | |
} else if (close) { | |
buffer.write("</${tag}>"); | |
} else {} | |
} | |
} | |
} | |
Set<String> _allowedHTMLTag = { | |
HtmlKeys.anchor, | |
HtmlKeys.bold, | |
HtmlKeys.unorderedList, | |
HtmlKeys.orderedList, | |
HtmlKeys.list, | |
HtmlKeys.blockquote, | |
HtmlKeys.horizontalRule, | |
HtmlKeys.italic, | |
HtmlKeys.h1, | |
HtmlKeys.h2, | |
HtmlKeys.h3, | |
HtmlKeys.image, | |
HtmlKeys.preformatted, | |
}; | |
void setDeltaAllowedTagForHTMLDecoder(Set<String> tagList) { | |
_allowedHTMLTag = tagList; | |
} | |
bool isAllowedHTML(Element elem) { | |
if (elem.localName == HtmlKeys.br && elem.children.isEmpty) { | |
return true; | |
} | |
final queue = Queue<Element>(); | |
queue.add(elem); | |
while (queue.isNotEmpty) { | |
final target = queue.removeFirst(); | |
if (!_allowedHTMLTag.contains(target.localName)) { | |
return false; | |
} | |
queue.addAll(target.children); | |
} | |
return true; | |
} | |
bool isInlineAttribute(String tag) { | |
return tag == HtmlKeys.anchor || | |
tag == HtmlKeys.bold || | |
tag == HtmlKeys.italic || | |
tag == HtmlKeys.horizontalRule || | |
tag == HtmlKeys.image; | |
} | |
class _HTMLNotusDecoder extends Converter<String, Delta> { | |
Map<String, Map<String, dynamic>> toDeltaAttribute(Queue<Element> elemStack) { | |
final deltaAttributeInline = <String, dynamic>{}; | |
final deltaAttributeLine = <String, dynamic>{}; | |
for (final elem in elemStack) { | |
switch (elem.localName) { | |
case HtmlKeys.image: | |
deltaAttributeInline[DeltaKeys.embed] = { | |
DeltaKeys.type: DeltaKeys.image, | |
DeltaKeys.imageSrc: elem.attributes[HtmlKeys.imageSrc], | |
}; | |
break; | |
case HtmlKeys.preformatted: | |
deltaAttributeLine[DeltaKeys.block] = DeltaKeys.code; | |
break; | |
case HtmlKeys.blockquote: | |
deltaAttributeLine[DeltaKeys.block] = DeltaKeys.quote; | |
break; | |
case HtmlKeys.horizontalRule: | |
deltaAttributeInline[DeltaKeys.embed] = { | |
DeltaKeys.type: DeltaKeys.hr | |
}; | |
break; | |
case HtmlKeys.list: | |
if (elem.parent.localName == HtmlKeys.orderedList) { | |
deltaAttributeLine[DeltaKeys.block] = DeltaKeys.ol; | |
} else if (elem.parent.localName == HtmlKeys.unorderedList) { | |
deltaAttributeLine[DeltaKeys.block] = DeltaKeys.ul; | |
} | |
break; | |
case HtmlKeys.orderedList: | |
case HtmlKeys.unorderedList: | |
break; | |
case HtmlKeys.h1: | |
case HtmlKeys.h2: | |
case HtmlKeys.h3: | |
deltaAttributeLine[DeltaKeys.heading] = int.parse(elem.localName[1]); | |
break; | |
case HtmlKeys.anchor: | |
deltaAttributeInline[DeltaKeys.a] = | |
elem.attributes[HtmlKeys.anchorHref]; | |
break; | |
case HtmlKeys.bold: | |
case HtmlKeys.italic: | |
deltaAttributeInline[elem.localName] = true; | |
break; | |
case HtmlKeys.br: | |
break; | |
default: | |
throw Exception("${elem.localName} not allowed"); | |
} | |
final attr = Map<String, dynamic>.from(elem.attributes); | |
if (elem.localName == HtmlKeys.anchor) { | |
attr.remove(HtmlKeys.anchorHref); | |
} | |
if (elem.localName == HtmlKeys.image) { | |
attr.remove(HtmlKeys.imageSrc); | |
} | |
} | |
return Map<String, Map<String, dynamic>>.from({ | |
Keys.inline: deltaAttributeInline, | |
Keys.line: deltaAttributeLine, | |
}); | |
} | |
List<Map<String, dynamic>> toDeltaFormatList(Element element) { | |
final deltaFormatList = <dynamic>[]; | |
void insert(idx, String text, elemStack) { | |
final attrMap = toDeltaAttribute(elemStack); | |
final attrLine = attrMap[Keys.line]; | |
final attrInline = attrMap[Keys.inline]; | |
if (text.isEmpty && attrInline.isEmpty && attrLine.isEmpty) { | |
return; | |
} | |
final int originalLength = deltaFormatList.length; | |
int shiftIdx() => idx + deltaFormatList.length - originalLength; | |
void insertText(txt) { | |
if (txt.isNotEmpty) { | |
if (attrInline.isEmpty) { | |
deltaFormatList.insert(shiftIdx(), {DeltaKeys.insert: txt}); | |
} else { | |
deltaFormatList.insert(shiftIdx(), | |
{DeltaKeys.insert: txt, DeltaKeys.attributes: attrInline}); | |
} | |
} else if (attrInline.containsKey("embed")) { | |
deltaFormatList.insert(shiftIdx(), { | |
DeltaKeys.insert: String.fromCharCode(8203), | |
DeltaKeys.attributes: attrInline | |
}); | |
} | |
if (attrLine.isNotEmpty && | |
(txt.isNotEmpty || attrInline.containsKey("embed"))) { | |
deltaFormatList.insert(shiftIdx(), | |
{DeltaKeys.insert: "\n", DeltaKeys.attributes: attrLine}); | |
} | |
} | |
if (attrLine.containsKey("block")) { | |
for (final lineText in text.split("\n")) { | |
insertText(lineText); | |
} | |
} else { | |
insertText(text); | |
} | |
} | |
final elemStack = Queue<Element>.from([element]); | |
if (element.children.isEmpty) { | |
if (element.localName == HtmlKeys.br) { | |
insert(0, "\n", Queue<Element>()); | |
} else { | |
insert(0, element.text, elemStack); | |
} | |
return List<Map<String, dynamic>>.from(deltaFormatList); | |
} | |
deltaFormatList.add(element); | |
final map = {element: elemStack}; | |
while (deltaFormatList.map((e) => e is Element).contains(true)) { | |
for (int i = 0; i < deltaFormatList.length; i++) { | |
final val = deltaFormatList[i]; | |
if ((val is Element) == false) { | |
continue; | |
} | |
deltaFormatList.removeAt(i); | |
final elem = val; | |
final currentElemStack = map[elem]; | |
int cursor = 0; | |
final htmlString = elem.innerHtml; | |
final int originalLength = deltaFormatList.length; | |
int shiftIdx() => deltaFormatList.length - originalLength; | |
for (var j = 0; j < elem.children.length; j++) { | |
final child = elem.children[j]; | |
final tagIdx = htmlString.indexOf(child.outerHtml, cursor); | |
final intervalText = htmlString.substring(cursor, tagIdx); | |
cursor = tagIdx + child.outerHtml.length; | |
if ((elem.localName != HtmlKeys.orderedList && | |
elem.localName != HtmlKeys.unorderedList) || | |
intervalText != '\n') { | |
insert(i + shiftIdx(), intervalText, currentElemStack); | |
} | |
final Queue<Element> newElemStack = Queue.from(currentElemStack); | |
newElemStack.addLast(child); | |
if (child.children.isNotEmpty) { | |
deltaFormatList.insert(i + shiftIdx(), child); | |
map[child] = newElemStack; | |
} else { | |
insert(i + shiftIdx(), child.text, newElemStack); | |
} | |
} | |
final lastInvervalText = | |
htmlString.substring(cursor, htmlString.length); | |
if ((elem.localName != HtmlKeys.orderedList && | |
elem.localName != HtmlKeys.unorderedList) || | |
lastInvervalText != '\n') { | |
insert(i + shiftIdx(), lastInvervalText, currentElemStack); | |
} | |
break; | |
} | |
} | |
return List<Map<String, dynamic>>.from(deltaFormatList); | |
} | |
Element getRootHTML(inputHTML) { | |
final parsedHTML = parse(inputHTML); | |
try { | |
return parsedHTML.children[0].children[1]; | |
} on RangeError catch (e) { | |
print(e); | |
return null; | |
} | |
} | |
@override | |
Delta convert(String inputHTML) { | |
final rootHTML = getRootHTML(inputHTML); | |
if (rootHTML == null || !rootHTML.hasContent()) { | |
return Delta()..insert("\n"); | |
} | |
final htmlString = rootHTML.innerHtml; | |
final deltaFormatList = <Map<String, dynamic>>[]; | |
void addPlainTextToDeltaList(text) { | |
if (text.isNotEmpty && text != String.fromCharCode(8203)) { | |
if (deltaFormatList.isNotEmpty && | |
deltaFormatList[deltaFormatList.length - 1][DeltaKeys.attributes] == | |
null) { | |
deltaFormatList[deltaFormatList.length - 1][DeltaKeys.insert] += text; | |
} else { | |
deltaFormatList | |
.add({DeltaKeys.insert: text, DeltaKeys.attributes: null}); | |
} | |
} | |
} | |
int cursor = 0; | |
for (Element firstLayerTag in rootHTML.children) { | |
final tagIdx = htmlString.indexOf(firstLayerTag.outerHtml, cursor); | |
final invervalText = htmlString.substring(cursor, tagIdx); | |
addPlainTextToDeltaList(invervalText); | |
cursor = tagIdx + firstLayerTag.outerHtml.length; | |
if (isAllowedHTML(firstLayerTag)) { | |
deltaFormatList.addAll(toDeltaFormatList(firstLayerTag)); | |
} else { | |
addPlainTextToDeltaList(firstLayerTag.outerHtml); | |
} | |
} | |
final lastInvervalText = htmlString.substring(cursor, htmlString.length); | |
addPlainTextToDeltaList(lastInvervalText); | |
void removeRedundantNewLine(deltaList) { | |
for (int i = 1; i < deltaList.length; i++) { | |
final prev = deltaList[i - 1]; | |
final current = deltaList[i]; | |
final text = current[DeltaKeys.insert]; | |
if (text.startsWith("\n")) { | |
final attr = prev[DeltaKeys.attributes]; | |
if (attr != null && | |
(attr.containsKey("block") || attr.containsKey("heading"))) { | |
if (text == "\n") { | |
deltaList.remove(current); | |
} else { | |
current[DeltaKeys.insert] = text.replaceFirst("\n", ""); | |
} | |
} | |
} | |
} | |
} | |
void avoidConcatinatingPlainTextAndLineAttributes(deltaList) { | |
bool isIncludeLineAttributes(Map<String, dynamic> jsonDelta) { | |
if (!jsonDelta.containsKey(DeltaKeys.attributes)) { | |
return false; | |
} | |
final attr = jsonDelta[DeltaKeys.attributes]; | |
if (attr == null) { | |
return false; | |
} | |
if (attr.containsKey(DeltaKeys.heading) || | |
attr.containsKey(DeltaKeys.quote) || | |
attr.containsKey(DeltaKeys.ol) || | |
attr.containsKey(DeltaKeys.ul)) { | |
return true; | |
} | |
return false; | |
} | |
for (int i = 1; i < deltaList.length - 1; i++) { | |
final next = deltaList[i + 1]; | |
final current = deltaList[i]; | |
final prev = deltaList[i - 1]; | |
if (isIncludeLineAttributes(next) && | |
!prev[DeltaKeys.insert].endsWith('\n')) { | |
current[DeltaKeys.insert] = '\n' + current[DeltaKeys.insert]; | |
} | |
} | |
} | |
void ensureEndWithNewLine(deltaList) { | |
final lastIndex = deltaList.length - 1; | |
final lastText = deltaList[lastIndex][DeltaKeys.insert]; | |
if (!lastText.endsWith("\n")) { | |
if (deltaList[lastIndex][DeltaKeys.attributes] != null) { | |
deltaList.add({DeltaKeys.insert: "\n"}); | |
} else { | |
deltaList[lastIndex][DeltaKeys.insert] = lastText + "\n"; | |
} | |
} | |
} | |
void removeZeroWidthSpaceFromNonEmbed(deltaList) { | |
for (Map<String, dynamic> deltaFormat in deltaList) { | |
final attr = deltaFormat[DeltaKeys.attributes]; | |
if (attr != null && attr.containsKey(DeltaKeys.embed)) { | |
continue; | |
} | |
String text = deltaFormat[DeltaKeys.insert]; | |
text = text.replaceAll(String.fromCharCode(8203), ""); | |
deltaFormat[DeltaKeys.insert] = text; | |
} | |
} | |
void insertNewlineAfterConsecutiveAnchorTagWithImage(deltaList) { | |
for (int i = 0; i < deltaList.length; i++) { | |
final prev = (i == 0) ? null : deltaList[i - 1]; | |
final next = (i == deltaList.length - 1) ? null : deltaList[i + 1]; | |
final current = deltaList[i]; | |
final nextAttr = | |
(i == deltaList.length - 1) ? null : next[DeltaKeys.attributes]; | |
final currentAttr = current[DeltaKeys.attributes]; | |
final prevAttr = (i == 0) ? null : prev[DeltaKeys.attributes]; | |
if (currentAttr == null || | |
!currentAttr.containsKey(DeltaKeys.a) || | |
!currentAttr.containsKey(DeltaKeys.embed) || | |
currentAttr[DeltaKeys.embed][DeltaKeys.type] != DeltaKeys.image) { | |
continue; | |
} | |
if (nextAttr != null && nextAttr.containsKey(DeltaKeys.a)) { | |
current[DeltaKeys.insert] = current[DeltaKeys.insert] + "\n"; | |
} | |
if (prevAttr != null && prevAttr.containsKey(DeltaKeys.a)) { | |
current[DeltaKeys.insert] = current[DeltaKeys.insert] + "\n"; | |
prev[DeltaKeys.insert] = prev[DeltaKeys.insert] + "\n"; | |
} | |
} | |
} | |
removeRedundantNewLine(deltaFormatList); | |
avoidConcatinatingPlainTextAndLineAttributes(deltaFormatList); | |
removeZeroWidthSpaceFromNonEmbed(deltaFormatList); | |
insertNewlineAfterConsecutiveAnchorTagWithImage(deltaFormatList); | |
ensureEndWithNewLine(deltaFormatList); | |
final delta = Delta.fromJson(deltaFormatList); | |
return delta; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment