#include "panels/QuillDelta.hpp" #include #include #include #include #include #include namespace geopro::app { namespace { // ── 序列化方向(QTextDocument → Delta)───────────────────────────────────── // 颜色转 Quill 习惯的小写 #rrggbb(与原版 ql-color/ql-background 选项一致)。 QString hexOf(const QColor& c) { return c.name(QColor::HexRgb); } // 行内样式 → attributes 对象(bold/italic/underline/color/background/size)。 QJsonObject inlineAttrs(const QTextCharFormat& fmt) { QJsonObject a; if (fmt.fontWeight() >= QFont::Bold) a[QStringLiteral("bold")] = true; if (fmt.fontItalic()) a[QStringLiteral("italic")] = true; if (fmt.fontUnderline()) a[QStringLiteral("underline")] = true; if (fmt.foreground().style() != Qt::NoBrush) a[QStringLiteral("color")] = hexOf(fmt.foreground().color()); if (fmt.background().style() != Qt::NoBrush) a[QStringLiteral("background")] = hexOf(fmt.background().color()); const double pt = fmt.fontPointSize(); if (pt > 0.0) // 以 px 表达(原版 ql-size 用 "NNpx";pt→px 约 *4/3 取整)。 a[QStringLiteral("size")] = QStringLiteral("%1px").arg(qRound(pt * 4.0 / 3.0)); return a; } // 块级样式 → attributes 对象(header/list/align),挂在换行 op 上。 QJsonObject blockAttrs(const QTextBlock& block) { QJsonObject a; const int headingLevel = block.blockFormat().headingLevel(); if (headingLevel >= 1 && headingLevel <= 4) a[QStringLiteral("header")] = headingLevel; if (QTextList* lst = block.textList()) { const QTextListFormat::Style s = lst->format().style(); a[QStringLiteral("list")] = (s == QTextListFormat::ListDecimal) ? QStringLiteral("ordered") : QStringLiteral("bullet"); } switch (block.blockFormat().alignment() & Qt::AlignHorizontal_Mask) { case Qt::AlignHCenter: a[QStringLiteral("align")] = QStringLiteral("center"); break; case Qt::AlignRight: a[QStringLiteral("align")] = QStringLiteral("right"); break; case Qt::AlignJustify: a[QStringLiteral("align")] = QStringLiteral("justify"); break; default: break; // 左对齐为默认,不写 attribute。 } return a; } // 追加一个文本 op(带可选 attributes)。 void pushInsert(QJsonArray& ops, const QString& text, const QJsonObject& attrs) { if (text.isEmpty()) return; QJsonObject op{{QStringLiteral("insert"), text}}; if (!attrs.isEmpty()) op[QStringLiteral("attributes")] = attrs; ops.append(op); } // 追加一个「换行」op(带可选块级 attributes)。Quill 中块级样式作用于其前整行。 void pushNewline(QJsonArray& ops, const QJsonObject& attrs) { pushInsert(ops, QStringLiteral("\n"), attrs); } // 序列化单个文本块的所有 fragment(按行内样式分段)。 void serializeBlock(const QTextBlock& block, QJsonArray& ops) { for (auto it = block.begin(); it != block.end(); ++it) { const QTextFragment frag = it.fragment(); if (frag.isValid()) pushInsert(ops, frag.text(), inlineAttrs(frag.charFormat())); } } // ── 反序列化方向(Delta → QTextDocument)────────────────────────────────── // 把 inline attributes 应用到字符格式。 void applyInlineAttrs(const QJsonObject& attrs, QTextCharFormat& fmt) { if (attrs.value(QStringLiteral("bold")).toBool()) fmt.setFontWeight(QFont::Bold); if (attrs.value(QStringLiteral("italic")).toBool()) fmt.setFontItalic(true); if (attrs.value(QStringLiteral("underline")).toBool()) fmt.setFontUnderline(true); const QString color = attrs.value(QStringLiteral("color")).toString(); if (QColor(color).isValid()) fmt.setForeground(QColor(color)); const QString bg = attrs.value(QStringLiteral("background")).toString(); if (QColor(bg).isValid()) fmt.setBackground(QColor(bg)); QString size = attrs.value(QStringLiteral("size")).toString(); if (size.endsWith(QStringLiteral("px"))) { const double px = size.chopped(2).toDouble(); if (px > 0.0) fmt.setFontPointSize(px * 3.0 / 4.0); // px→pt。 } } // 把 block attributes 应用到块格式 / 列表(作用于换行前的当前块)。 void applyBlockAttrs(const QJsonObject& attrs, QTextCursor& cur) { QTextBlockFormat bf = cur.blockFormat(); const int header = attrs.value(QStringLiteral("header")).toInt(); if (header >= 1 && header <= 4) bf.setHeadingLevel(header); const QString align = attrs.value(QStringLiteral("align")).toString(); if (align == QStringLiteral("center")) bf.setAlignment(Qt::AlignHCenter); else if (align == QStringLiteral("right")) bf.setAlignment(Qt::AlignRight); else if (align == QStringLiteral("justify")) bf.setAlignment(Qt::AlignJustify); cur.setBlockFormat(bf); const QString list = attrs.value(QStringLiteral("list")).toString(); if (list == QStringLiteral("ordered")) cur.createList(QTextListFormat::ListDecimal); else if (list == QStringLiteral("bullet")) cur.createList(QTextListFormat::ListDisc); } // 写入一段不含换行的文本(带行内样式)。 void insertSegment(QTextCursor& cur, const QString& text, const QJsonObject& attrs) { QTextCharFormat fmt; applyInlineAttrs(attrs, fmt); cur.insertText(text, fmt); } // 处理单个 insert op:按换行拆段。每个换行:先对当前块应用块级样式(header/list/align), // 再开新块承接后续内容。Delta 末尾必有一个收尾换行,会多造一个尾部空块,由 // deltaToDocument 末尾统一裁掉(与 QTextDocument 起始即含一个空块的语义对齐)。 void applyOp(const QString& insert, const QJsonObject& attrs, QTextCursor& cur) { const QStringList lines = insert.split(QLatin1Char('\n')); for (int i = 0; i < lines.size(); ++i) { insertSegment(cur, lines.at(i), attrs); if (i + 1 < lines.size()) { // 该位置原本是一个换行符。 applyBlockAttrs(attrs, cur); // 块级样式作用于换行之前的整行。 cur.insertBlock(); } } } // 裁掉文档末尾因 Delta 收尾换行而多出的空块(仅当其确为空且非唯一块)。 void trimTrailingEmptyBlock(QTextDocument& doc) { if (doc.blockCount() <= 1) return; const QTextBlock last = doc.lastBlock(); if (!last.text().isEmpty()) return; QTextCursor cur(&doc); cur.movePosition(QTextCursor::End); cur.deletePreviousChar(); // 删除上一个块结尾的换行,合并末尾空块。 } } // namespace QJsonArray documentToDelta(const QTextDocument& doc) { QJsonArray ops; for (QTextBlock block = doc.begin(); block.isValid(); block = block.next()) { serializeBlock(block, ops); // 每个块以换行 op 收尾,携带该块的块级样式(最后一个块也写,对应 Quill 末尾换行)。 pushNewline(ops, blockAttrs(block)); } return ops; } void deltaToDocument(const QJsonArray& ops, QTextDocument& doc) { doc.clear(); QTextCursor cur(&doc); for (const QJsonValue& v : ops) { const QJsonObject op = v.toObject(); const QJsonValue insert = op.value(QStringLiteral("insert")); if (!insert.isString()) continue; // 仅支持文本 insert(图片/嵌入等降级丢弃)。 applyOp(insert.toString(), op.value(QStringLiteral("attributes")).toObject(), cur); } trimTrailingEmptyBlock(doc); } } // namespace geopro::app