import 'package:vsc_quill_delta_to_html/vsc_quill_delta_to_html.dart'; import 'package:html/parser.dart' as html_parser; import 'package:html/dom.dart' as dom; /// Converts a Quill Delta (as JSON ops) into an HTML string that survives a /// round-trip back through [HtmlToDelta] (see translation_input_container.dart). /// /// Two issues in the underlying libraries are worked around here: /// - flutter_quill stores colors as 8-digit ARGB hex (#AARRGGBB), which /// vsc_quill_delta_to_html's sanitizer silently drops unless truncated to /// #RRGGBB (colors picked in this editor are always opaque). /// - vsc_quill_delta_to_html attaches a `style` attribute (color/background) /// to whichever inline tag (e.g. ) comes first, but /// flutter_quill_delta_from_html only reads `style` off elements. /// Combined bold/italic + color would otherwise render fine everywhere /// else but vanish when the editor reopens the HTML. Wrapping any styled /// non-span element in a fixes the round-trip. String quillOpsToHtml(List ops) { final normalizedOps = _normalizeColors(ops); final html = QuillDeltaToHtmlConverter( List>.from(normalizedOps), ConverterOptions( converterOptions: OpConverterOptions(inlineStylesFlag: true), ), ).convert(); return _moveStyleToWrappingSpan(html); } List> _normalizeColors(List ops) { return ops.map((op) { final copy = Map.from(op); final attrs = copy['attributes']; if (attrs is Map) { final attrsCopy = Map.from(attrs); for (final key in ['color', 'background']) { final value = attrsCopy[key]; if (value is String && value.length == 9 && value.startsWith('#')) { attrsCopy[key] = '#${value.substring(3)}'; } } copy['attributes'] = attrsCopy; } return copy; }).toList(); } String _moveStyleToWrappingSpan(String htmlStr) { final fragment = html_parser.parseFragment(htmlStr); void visit(dom.Node node) { if (node is dom.Element) { if (node.localName != 'span' && node.attributes.containsKey('style')) { final style = node.attributes.remove('style'); final span = dom.Element.tag('span')..attributes['style'] = style!; final parent = node.parent!; final index = parent.nodes.indexOf(node); parent.nodes ..removeAt(index) ..insert(index, span); span.nodes.add(node); } for (final child in List.from(node.nodes)) { visit(child); } } } for (final child in List.from(fragment.nodes)) { visit(child); } return fragment.outerHtml; }