manager-app/lib/Helpers/quill_html_converter.dart
Thomas Fransolet a54952e5f4 MISC
2026-07-10 16:54:26 +02:00

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;
}