74 lines
2.7 KiB
Dart
74 lines
2.7 KiB
Dart
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. <strong>) comes first, but
|
|
/// flutter_quill_delta_from_html only reads `style` off <span> 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 <span> fixes the round-trip.
|
|
String quillOpsToHtml(List<dynamic> ops) {
|
|
final normalizedOps = _normalizeColors(ops);
|
|
final html = QuillDeltaToHtmlConverter(
|
|
List<Map<String, dynamic>>.from(normalizedOps),
|
|
ConverterOptions(
|
|
converterOptions: OpConverterOptions(inlineStylesFlag: true),
|
|
),
|
|
).convert();
|
|
return _moveStyleToWrappingSpan(html);
|
|
}
|
|
|
|
List<Map<String, dynamic>> _normalizeColors(List<dynamic> ops) {
|
|
return ops.map((op) {
|
|
final copy = Map<String, dynamic>.from(op);
|
|
final attrs = copy['attributes'];
|
|
if (attrs is Map) {
|
|
final attrsCopy = Map<String, dynamic>.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<dom.Node>.from(node.nodes)) {
|
|
visit(child);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (final child in List<dom.Node>.from(fragment.nodes)) {
|
|
visit(child);
|
|
}
|
|
|
|
return fragment.outerHtml;
|
|
}
|