File indexing completed on 2025-02-23 04:35:29

0001 /*
0002     SPDX-FileCopyrightText: 2020-2022 Mladen Milinkovic <max@smoothware.net>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "richdocument.h"
0008 
0009 #include "core/richtext/richdocumentlayout.h"
0010 #include "core/richtext/richdom.h"
0011 #include "helpers/common.h"
0012 
0013 #include <QApplication>
0014 #include <QPainter>
0015 #include <QSharedPointer>
0016 #include <QSet>
0017 #include <QStyle>
0018 #include <QStyleOptionViewItem>
0019 #include <QTextDocumentFragment>
0020 #include <QTextBlock>
0021 
0022 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0023 // QStringView use is unoptimized in Qt5, and some methods are missing pre 5.15
0024 #include <QStringRef>
0025 #define QStringView(x) QStringRef(&(x))
0026 #define QStringView_ QStringRef
0027 #define capturedView capturedRef
0028 #else
0029 #include <QStringView>
0030 #define QStringView_ QStringView
0031 #endif
0032 
0033 
0034 #define DEBUG_CHANGES(...) //qDebug(__VA_ARGS__)
0035 
0036 using namespace SubtitleComposer;
0037 
0038 struct REStringCapture { int pos; int len; int no; };
0039 Q_DECLARE_TYPEINFO(REStringCapture, Q_PRIMITIVE_TYPE);
0040 
0041 struct REBackrefFragment {
0042     REBackrefFragment() : pos(-1), len(-1) {}
0043     int pos; int len; QSharedPointer<QTextDocumentFragment> frag;
0044 };
0045 
0046 
0047 enum EditType { None, ReplaceChar, ReplacePlain, ReplaceHtml, ReplaceFragment, Delete };
0048 struct EditChange {
0049     EditChange() : type(None) {}
0050     EditChange(EditType t, int p, int l) : type(t), pos(p), len(l) {}
0051     EditChange(EditType t, int p, int l, const QChar &v) : type(t), pos(p), len(l), qchar(v.unicode()) {}
0052     EditChange(EditType t, int p, int l, QStringView_ v) : type(t), pos(p), len(l), qstr(v) {}
0053     EditChange(EditType t, int p, int l, const QSharedPointer<QTextDocumentFragment> v) : type(t), pos(p), len(l), qfrag(v) {}
0054     EditChange(EditType t, int p, int l, const QTextCursor &c) : type(t), pos(p), len(l), qfrag(new QTextDocumentFragment(c)) {}
0055     EditChange(EditType t, int p, int l, const QTextDocument &d) : type(t), pos(p), len(l), qfrag(new QTextDocumentFragment(&d)) {}
0056     EditType type;
0057     int pos;
0058     int len;
0059     ushort qchar = 0;
0060     QStringView_ qstr;
0061     QSharedPointer<QTextDocumentFragment> qfrag;
0062 };
0063 
0064 
0065 RichDocument::RichDocument(QObject *parent)
0066     : QTextDocument(parent),
0067       m_undoableCursor(this),
0068       m_stylesheet(nullptr),
0069       m_domDirty(true),
0070       m_dom(new RichDOM)
0071 {
0072     setUndoRedoEnabled(true);
0073 
0074     QTextOption textOption;
0075     textOption.setAlignment(Qt::AlignCenter);
0076     textOption.setWrapMode(QTextOption::NoWrap);
0077     setDefaultTextOption(textOption);
0078 
0079     setDefaultStyleSheet($("p { display:block; white-space:pre; margin-top:0; margin-bottom:0; }"));
0080 
0081     setDocumentLayout(new RichDocumentLayout(this));
0082 
0083     connect(this, &RichDocument::contentsChanged, this, [&](){
0084         m_domDirty = true;
0085         emit domChanged();
0086     });
0087 }
0088 
0089 RichDocument::~RichDocument()
0090 {
0091     delete m_dom;
0092 }
0093 
0094 void
0095 RichDocument::markStylesheetDirty()
0096 {
0097     m_undoableCursor.beginEditBlock();
0098     markContentsDirty(0, length());
0099     m_undoableCursor.endEditBlock();
0100 }
0101 
0102 void
0103 RichDocument::setStylesheet(const RichCSS *css)
0104 {
0105     if(m_stylesheet == css)
0106         return;
0107     if(m_stylesheet)
0108         disconnect(m_stylesheet, &RichCSS::changed, this, &RichDocument::markStylesheetDirty);
0109     m_stylesheet = css;
0110     if(m_stylesheet)
0111         connect(m_stylesheet, &RichCSS::changed, this, &RichDocument::markStylesheetDirty);
0112     markStylesheetDirty();
0113 }
0114 
0115 void
0116 RichDocument::setRichText(const RichString &text, bool resetUndo)
0117 {
0118     if(resetUndo)
0119         setUndoRedoEnabled(false);
0120     else
0121         m_undoableCursor.beginEditBlock();
0122 
0123     m_undoableCursor.select(QTextCursor::Document);
0124     m_undoableCursor.removeSelectedText();
0125 
0126     int currentStyleFlags = -1;
0127     QRgb currentStyleColor = 0;
0128     QSet<QString> currentStyleClasses;
0129     QString currentStyleVoice;
0130     QTextCharFormat format;
0131     int prev = 0;
0132     for(int pos = 0, size = text.length(); pos < size; pos++) {
0133         const int posFlags = text.styleFlagsAt(pos);
0134         const QRgb posColor = text.styleColorAt(pos);
0135         const QSet<QString> posClasses = text.styleClassesAt(pos);
0136         const QString posVoice = text.styleVoiceAt(pos);
0137         if(currentStyleFlags != posFlags || ((posFlags & SubtitleComposer::RichString::Color) && currentStyleColor != posColor)) {
0138             if(prev != pos) {
0139                 m_undoableCursor.insertText(text.string().mid(prev, pos - prev), format);
0140                 prev = pos;
0141             }
0142             currentStyleFlags = posFlags;
0143             currentStyleColor = posColor;
0144             format.setFontWeight(currentStyleFlags & SubtitleComposer::RichString::Bold ? QFont::Bold : QFont::Normal);
0145             format.setFontItalic(currentStyleFlags & SubtitleComposer::RichString::Italic);
0146             format.setFontUnderline(currentStyleFlags & SubtitleComposer::RichString::Underline);
0147             format.setFontStrikeOut(currentStyleFlags & SubtitleComposer::RichString::StrikeThrough);
0148             if((currentStyleFlags & SubtitleComposer::RichString::Color) == 0)
0149                 format.setForeground(QBrush());
0150             else
0151                 format.setForeground(QBrush(QColor(currentStyleColor)));
0152         }
0153         if(currentStyleClasses != posClasses) {
0154             if(prev != pos) {
0155                 m_undoableCursor.insertText(text.string().mid(prev, pos - prev), format);
0156                 prev = pos;
0157             }
0158             currentStyleClasses = posClasses;
0159             format.setProperty(RichDocument::Class, QVariant::fromValue(text.styleClassesAt(pos)));
0160         }
0161         if(currentStyleVoice != posVoice) {
0162             if(prev != pos) {
0163                 m_undoableCursor.insertText(text.string().mid(prev, pos - prev), format);
0164                 prev = pos;
0165             }
0166             currentStyleVoice = posVoice;
0167             format.setProperty(RichDocument::Voice, QVariant::fromValue(text.styleVoiceAt(pos)));
0168         }
0169     }
0170     if(prev != text.length())
0171         m_undoableCursor.insertText(text.string().mid(prev), format);
0172 
0173     if(resetUndo)
0174         setUndoRedoEnabled(true);
0175     else
0176         m_undoableCursor.endEditBlock();
0177 }
0178 
0179 QString
0180 RichDocument::toHtml() const
0181 {
0182     QString html;
0183     bool fB = false;
0184     bool fI = false;
0185     bool fU = false;
0186     bool fS = false;
0187     QRgb fC = 0;
0188     QSet<QString> fClass;
0189     QString fVoice; // <v:speaker name> - can't be nested... right? No need for QSet<QString>
0190     QTextBlock bi = begin();
0191     for(;;) {
0192         for(QTextBlock::iterator it = bi.begin(); !it.atEnd(); ++it) {
0193             const QTextFragment &f = it.fragment();
0194             if(!f.isValid())
0195                 continue;
0196             const QTextCharFormat &format = f.charFormat();
0197             const QSet<QString> &cl = format.property(Class).value<QSet<QString>>();
0198             for(auto it = fClass.begin(); it != fClass.end();) {
0199                 if(cl.contains(*it)) {
0200                     ++it;
0201                     continue;
0202                 }
0203                 html.append($("</c.%1>").arg(*it));
0204                 it = fClass.erase(it);
0205             }
0206             for(auto it = cl.cbegin(); it != cl.cend(); ++it) {
0207                 if(fClass.contains(*it))
0208                     continue;
0209                 html.append($("<c.%1>").arg(*it));
0210                 fClass.insert(*it);
0211             }
0212             const QString &vt = format.property(Voice).value<QString>();
0213             if(fVoice != vt) {
0214                 fVoice = vt;
0215                 html.append($("<v %1>").arg(fVoice));
0216             }
0217             if(fB != (format.fontWeight() == QFont::Bold))
0218                 html.append((fB = !fB) ? $("<b>") : $("</b>"));
0219             if(fI != format.fontItalic())
0220                 html.append((fI = !fI) ? $("<i>") : $("</i>"));
0221             if(fU != format.fontUnderline())
0222                 html.append((fU = !fU) ? $("<u>") : $("</u>"));
0223             if(fS != format.fontStrikeOut())
0224                 html.append((fS = !fS) ? $("<s>") : $("</s>"));
0225             const QRgb fg = format.foreground().style() != Qt::NoBrush ? format.foreground().color().toRgb().rgb() : 0;
0226             if(fC != fg) {
0227                 if(fC) html.append($("</font>"));
0228                 if((fC = fg)) html.append($("<font color=#%1>").arg(fC & 0xFFFFFF, 6, 16, QChar('0')));
0229             }
0230             html.append(f.text().replace(QChar::LineSeparator, $("<br>\n")));
0231         }
0232         bi = bi.next();
0233         if(bi == end()) {
0234             if(fB) html.append($("</b>"));
0235             if(fI) html.append($("</i>"));
0236             if(fU) html.append($("</u>"));
0237             if(fS) html.append($("</s>"));
0238             if(fC) html.append($("</font>"));
0239             for(const QString &cl: fClass)
0240                 html.append($("</c.%1>").arg(cl));
0241             return html;
0242         }
0243         html.append($("<br>\n"));
0244     }
0245     // unreachable
0246 }
0247 
0248 void
0249 RichDocument::linesToBlocks()
0250 {
0251     for(QTextBlock bi = begin(); bi != end(); bi = bi.next()) {
0252         const QString &text = bi.text();
0253         for(int i = 0; i < text.length(); i++) {
0254             if(text.at(i).unicode() == QChar::LineSeparator) {
0255                 m_undoableCursor.movePosition(QTextCursor::Start);
0256                 m_undoableCursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, bi.position() + i);
0257                 m_undoableCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, 1);
0258                 m_undoableCursor.insertBlock();
0259                 break;
0260             }
0261         }
0262     }
0263 }
0264 
0265 void
0266 RichDocument::setHtml(const QString &html, bool resetUndo)
0267 {
0268     RichString s;
0269     s.setRichString(html);
0270     setRichText(s, resetUndo);
0271 }
0272 
0273 void
0274 RichDocument::setPlainText(const QString &text, bool resetUndo)
0275 {
0276     if(resetUndo)
0277         setUndoRedoEnabled(false);
0278     else
0279         m_undoableCursor.beginEditBlock();
0280     m_undoableCursor.select(QTextCursor::Document);
0281     m_undoableCursor.insertText(text);
0282     linesToBlocks();
0283     if(resetUndo)
0284         setUndoRedoEnabled(true);
0285     else
0286         m_undoableCursor.endEditBlock();
0287 }
0288 
0289 void
0290 RichDocument::setDocument(const QTextDocument *doc, bool resetUndo)
0291 {
0292     if(resetUndo)
0293         setUndoRedoEnabled(false);
0294     else
0295         m_undoableCursor.beginEditBlock();
0296     m_undoableCursor.select(QTextCursor::Document);
0297     QTextCursor cur(const_cast<QTextDocument *>(doc));
0298     cur.select(QTextCursor::Document);
0299     m_undoableCursor.insertFragment(cur.selection());
0300     linesToBlocks();
0301     if(resetUndo)
0302         setUndoRedoEnabled(true);
0303     else
0304         m_undoableCursor.endEditBlock();
0305 }
0306 
0307 void
0308 RichDocument::clear(bool resetUndo)
0309 {
0310     if(resetUndo)
0311         setUndoRedoEnabled(false);
0312     else
0313         m_undoableCursor.beginEditBlock();
0314     m_undoableCursor.select(QTextCursor::Document);
0315     m_undoableCursor.removeSelectedText();
0316     if(resetUndo)
0317         setUndoRedoEnabled(true);
0318     else
0319         m_undoableCursor.endEditBlock();
0320 }
0321 
0322 RichString
0323 RichDocument::toRichText() const
0324 {
0325     SubtitleComposer::RichString richText;
0326 
0327     for(QTextBlock bi = begin(); bi != end(); bi = bi.next()) {
0328         if(bi != begin())
0329             richText.append(QChar::LineFeed);
0330         for(QTextBlock::iterator it = bi.begin(); !it.atEnd(); ++it) {
0331             const QTextFragment &f = it.fragment();
0332             if(!f.isValid())
0333                 continue;
0334             const QTextCharFormat &format = f.charFormat();
0335             int styleFlags = 0;
0336             QRgb styleColor;
0337             const QSet<QString> &styleClass = format.property(Class).value<QSet<QString>>();
0338             const QString &styleVoice = format.property(Voice).value<QString>();
0339             if(format.fontWeight() == QFont::Bold)
0340                 styleFlags |= SubtitleComposer::RichString::Bold;
0341             if(format.fontItalic())
0342                 styleFlags |= SubtitleComposer::RichString::Italic;
0343             if(format.fontUnderline())
0344                 styleFlags |= SubtitleComposer::RichString::Underline;
0345             if(format.fontStrikeOut())
0346                 styleFlags |= SubtitleComposer::RichString::StrikeThrough;
0347             if(format.foreground().style() != Qt::NoBrush) {
0348                 styleFlags |= SubtitleComposer::RichString::Color;
0349                 styleColor = format.foreground().color().toRgb().rgb();
0350             } else {
0351                 styleColor = 0;
0352             }
0353 
0354             richText.append(RichString(f.text(), styleFlags, styleColor, styleClass, styleVoice));
0355         }
0356     }
0357     return richText;
0358 }
0359 
0360 void
0361 RichDocument::joinLines()
0362 {
0363     m_undoableCursor.beginEditBlock();
0364     m_undoableCursor.movePosition(QTextCursor::Start);
0365     for(;;) {
0366         if(!m_undoableCursor.movePosition(QTextCursor::EndOfLine, QTextCursor::MoveAnchor)
0367         || !m_undoableCursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, 1))
0368             break;
0369         m_undoableCursor.insertText($(" "));
0370     }
0371     m_undoableCursor.endEditBlock();
0372 }
0373 
0374 void
0375 RichDocument::replace(QChar before, QChar after, Qt::CaseSensitivity cs)
0376 {
0377     const QString search(before);
0378     const FindFlags ff = cs == Qt::CaseSensitive ? FindCaseSensitively : FindFlags();
0379 
0380     m_undoableCursor.beginEditBlock();
0381     m_undoableCursor.movePosition(QTextCursor::Start);
0382     for(;;) {
0383         m_undoableCursor = find(search, m_undoableCursor, ff);
0384         if(m_undoableCursor.isNull())
0385             break;
0386         m_undoableCursor.insertText(after);
0387     }
0388     m_undoableCursor.endEditBlock();
0389 }
0390 
0391 int
0392 RichDocument::indexOf(const QRegularExpression &re, int from)
0393 {
0394     QTextCursor cursor = find(re, from);
0395     return cursor.isNull() ? -1 : cursor.position();
0396 }
0397 
0398 int
0399 RichDocument::cummulativeStyleFlags() const
0400 {
0401     int flags = 0;
0402     for(QTextBlock bi = begin(); bi != end(); bi = bi.next()) {
0403         for(QTextBlock::iterator it = bi.begin(); !it.atEnd(); ++it) {
0404             const QTextFragment &f = it.fragment();
0405             if(!f.isValid())
0406                 continue;
0407             const QTextCharFormat &format = f.charFormat();
0408             // FIXME: consider classes/styles/css
0409             if(format.fontWeight() == QFont::Bold)
0410                 flags |= SubtitleComposer::RichString::Bold;
0411             if(format.fontItalic())
0412                 flags |= SubtitleComposer::RichString::Italic;
0413             if(format.fontUnderline())
0414                 flags |= SubtitleComposer::RichString::Underline;
0415             if(format.fontStrikeOut())
0416                 flags |= SubtitleComposer::RichString::StrikeThrough;
0417             if(format.foreground().style() != Qt::NoBrush)
0418                 flags |= SubtitleComposer::RichString::Color;
0419         }
0420     }
0421     return flags;
0422 }
0423 
0424 QRgb
0425 RichDocument::styleColorAt(int index) const
0426 {
0427     QTextCursor c(const_cast<RichDocument *>(this));
0428     c.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, index);
0429     return c.charFormat().foreground().color().rgba();
0430 }
0431 
0432 void
0433 RichDocument::applyChanges(const void *data)
0434 {
0435     auto changeList = reinterpret_cast<const QVector<EditChange> *>(data);
0436     m_undoableCursor.beginEditBlock();
0437     DEBUG_CHANGES("** BEGIN '%s'", toPlainText().toUtf8().constData());
0438     for(auto it = changeList->crbegin(); it != changeList->crend(); ++it) {
0439         m_undoableCursor.movePosition(QTextCursor::Start);
0440         if(it->pos)
0441             m_undoableCursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, it->pos);
0442         if(it->len)
0443             m_undoableCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, it->len);
0444         switch(it->type) {
0445         case Delete:
0446             DEBUG_CHANGES("Delete %d-%d '%s'", it->pos, it->pos + it->len,
0447                           m_undoableCursor.selectedText().toUtf8().constData());
0448             m_undoableCursor.removeSelectedText();
0449             break;
0450         case ReplaceChar:
0451             DEBUG_CHANGES("ReplaceChar %d-%d '%s' <- '%s'", it->pos, it->pos + it->len,
0452                           m_undoableCursor.selectedText().toUtf8().constData(),
0453                           QString(QChar(it->qchar)).toUtf8().constData());
0454             m_undoableCursor.insertText(QString(QChar(it->qchar)));
0455             break;
0456         case ReplacePlain:
0457             DEBUG_CHANGES("ReplacePlain %d-%d '%s' <- '%s'", it->pos, it->pos + it->len,
0458                           m_undoableCursor.selectedText().toUtf8().constData(),
0459                           it->qstr.toUtf8().constData());
0460             m_undoableCursor.insertText(it->qstr.toString());
0461             break;
0462         case ReplaceHtml:
0463             DEBUG_CHANGES("ReplaceHtml %d-%d '%s' <- '%s'", it->pos, it->pos + it->len,
0464                           m_undoableCursor.selectedText().toUtf8().constData(),
0465                           it->qstr.toUtf8().constData());
0466             m_undoableCursor.insertHtml(it->qstr.toString());
0467             break;
0468         case ReplaceFragment:
0469             DEBUG_CHANGES("ReplaceFragment %d-%d '%s' <- '%s'", it->pos, it->pos + it->len,
0470                           m_undoableCursor.selectedText().toUtf8().constData(),
0471                           it->qfrag->toPlainText().toUtf8().constData());
0472             m_undoableCursor.insertFragment(*it->qfrag);
0473             break;
0474         default:
0475             Q_ASSERT(false);
0476         }
0477     }
0478     DEBUG_CHANGES("** END '%s'", toPlainText().toUtf8().constData());
0479     m_undoableCursor.endEditBlock();
0480 }
0481 
0482 void
0483 RichDocument::cleanupSpaces()
0484 {
0485     QVector<EditChange> cl;
0486 
0487     int pos = 0;
0488     for(QTextBlock bi = begin(); bi != end(); bi = bi.next()) {
0489         const QString &text = bi.text();
0490         const int textLen = text.length();
0491         int usefulLen = textLen;
0492 
0493         // ignore space at the end of the line
0494         while(usefulLen > 0 && text.at(usefulLen - 1).isSpace())
0495             usefulLen--;
0496 
0497         if(!usefulLen) { // remove empty line
0498             cl.push_back(EditChange(Delete, pos ? pos - 1 : pos, textLen + 1));
0499             pos += textLen + 1;
0500             continue;
0501         }
0502 
0503         bool lastWasSpace = true;
0504         for(int i = 0; i < usefulLen; i++) {
0505             const QChar &cc = text.at(i);
0506             const bool thisIsSpace = cc.isSpace();
0507             if(lastWasSpace && thisIsSpace) { // remove consecutive spaces and spaces at the start of the line
0508                 cl.push_back(EditChange(Delete, pos + i, 1));
0509                 continue;
0510             }
0511             if(thisIsSpace && cc.unicode() != QChar::Space) { // tabs etc to space
0512                 cl.push_back(EditChange(ReplaceChar, pos + i, 1, QChar(QChar::Space)));
0513                 lastWasSpace = true;
0514                 continue;
0515             }
0516             lastWasSpace = thisIsSpace;
0517         }
0518 
0519         // remove space at the end of the line
0520         if(usefulLen != textLen)
0521             cl.push_back(EditChange(Delete, pos + usefulLen, textLen - usefulLen));
0522 
0523         pos += textLen + 1;
0524     }
0525 
0526     applyChanges(&cl);
0527 }
0528 
0529 void
0530 RichDocument::fixPunctuation(bool spaces, bool quotes, bool englishI, bool ellipsis, bool *cont, bool testOnly)
0531 {
0532     if(isEmpty())
0533         return;
0534 
0535     if(testOnly) {
0536         if(!cont)
0537             return;
0538         RichDocument tmp;
0539         tmp.setDocument(this, true);
0540         tmp.fixPunctuation(spaces, quotes, englishI, ellipsis, cont, false);
0541         return;
0542     }
0543 
0544     if(spaces)
0545         cleanupSpaces();
0546 
0547     if(quotes) { // quotes and double quotes
0548         staticRE$(reQ1, "`|´|\u0092", REs | REu);
0549         replace(reQ1, $("'"));
0550         staticRE$(reQ2, "''|«|»", REs | REu);
0551         replace(reQ2, $("\""));
0552     }
0553 
0554     if(spaces) {
0555         // remove spaces after " or ' at the beginning of line
0556         staticRE$(reS1, "^([\"'])\\s", REs | REu);
0557         replace(reS1, $("\\1"));
0558 
0559         // remove space before " or ' at the end of line
0560         staticRE$(reS2, "\\s([\"'])$", REs | REu);
0561         replace(reS2, $("\\1"));
0562 
0563         // if not present, add space after '?', '!', ',', ';', ':', ')' and ']'
0564         staticRE$(reS3, "([\\?!,;:\\)\\]])([^\\s\"'])", REs | REu);
0565         replace(reS3, $("\\1 \\2"));
0566 
0567         // if not present, add space after '.'
0568         staticRE$(reS4, "(\\.)([^\\s\\.\"'])", REs | REu);
0569         replace(reS4, $("\\1 \\2"));
0570 
0571         // remove space after '¿', '¡', '(' and '['
0572         staticRE$(reS5, "([¿¡\\(\\[])\\s", REs | REu);
0573         replace(reS5, $("\\1"));
0574 
0575         // remove space before '?', '!', ',', ';', ':', '.', ')' and ']'
0576         staticRE$(reS6, "\\s([\\?!,;:\\.\\)\\]])", REs | REu);
0577         replace(reS6, $("\\1"));
0578 
0579         // remove space after ... at the beginning of sentence
0580         staticRE$(reS7, "^\\.\\.\\.?\\s", REs | REu);
0581         replace(reS7, $("..."));
0582     }
0583 
0584     if(englishI) {
0585         // fix english I pronoun capitalization
0586         staticRE$(reI, "([\\s\"'\\(\\[])i([\\s'\",;:\\.\\?!\\]\\)]|$)" , REs | REu);
0587         replace(reI, $("\\1I\\2"));
0588     }
0589 
0590     if(ellipsis) {
0591         // fix ellipsis
0592         staticRE$(reE1, "[,;]?\\.{2,}", REs | REu);
0593         staticRE$(reE2, "[,;]\\s*$", REs | REu);
0594         staticRE$(reE3, "[\\.:?!\\)\\]'\\\"]$", REs | REu);
0595         replace(reE1, $("..."));
0596         replace(reE2, $("..."));
0597 
0598         if(indexOf(reE3) == -1) {
0599             undoableCursor()->movePosition(QTextCursor::End);
0600             undoableCursor()->insertText($("..."));
0601         }
0602 
0603         if(cont) {
0604             staticRE$(reE4, "^\\s*\\.{3}[^\\.]?", REs | REu);
0605             staticRE$(reE5, "^\\s*\\.*\\s*", REs | REu);
0606             staticRE$(reE6, "\\.{3,3}\\s*$", REs | REu);
0607             if(*cont && indexOf(reE4) == -1)
0608                 replace(reE5, $("..."));
0609 
0610             *cont = indexOf(reE6) != -1;
0611         }
0612     } else {
0613         if(cont) {
0614             staticRE$(reC1, "[?!\\)\\]'\\\"]\\s*$", REs | REu);
0615             staticRE$(reC2, "[^\\.]?\\.\\s*$", REs | REu);
0616             *cont = indexOf(reC1) == -1;
0617             if(!*cont)
0618                 *cont = indexOf(reC2) == -1;
0619         }
0620     }
0621 }
0622 
0623 void
0624 RichDocument::toLower()
0625 {
0626     m_undoableCursor.beginEditBlock();
0627     for(QTextBlock bi = begin(); bi != end(); bi = bi.next()) {
0628         const QString &text = bi.text();
0629         for(int i = 0; i < text.length(); i++) {
0630             if(text.at(i).isUpper()) {
0631                 m_undoableCursor.movePosition(QTextCursor::Start);
0632                 m_undoableCursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, bi.position() + i);
0633                 m_undoableCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, 1);
0634                 m_undoableCursor.insertText(QString(text.at(i).toLower()));
0635             }
0636         }
0637     }
0638     m_undoableCursor.endEditBlock();
0639 }
0640 
0641 void
0642 RichDocument::toUpper()
0643 {
0644     m_undoableCursor.beginEditBlock();
0645     for(QTextBlock bi = begin(); bi != end(); bi = bi.next()) {
0646         const QString &text = bi.text();
0647         for(int i = 0; i < text.length(); i++) {
0648             if(text.at(i).isLower()) {
0649                 m_undoableCursor.movePosition(QTextCursor::Start);
0650                 m_undoableCursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, bi.position() + i);
0651                 m_undoableCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, 1);
0652                 m_undoableCursor.insertText(QString(text.at(i).toUpper()));
0653             }
0654         }
0655     }
0656     m_undoableCursor.endEditBlock();
0657 }
0658 
0659 void
0660 RichDocument::toSentenceCase(bool *isSentenceStart, bool convertLowerCase, bool titleCase, bool testOnly)
0661 {
0662     if(!testOnly)
0663         m_undoableCursor.beginEditBlock();
0664     for(QTextBlock bi = begin(); bi != end(); bi = bi.next()) {
0665         const QString &text = bi.text();
0666         bool wordStart = true;
0667         for(int i = 0; i < text.length(); i++) {
0668             const QChar &ch = text.at(i);
0669             const bool isSpace = ch.isSpace();
0670             const bool isEndPunct = !isSpace && (ch == QChar('.') || ch == QChar('?') || ch == QChar('!') || ch == QChar(ushort(0xbf)/*¿*/));
0671             if(!testOnly) {
0672                 if(titleCase ? wordStart : *isSentenceStart) {
0673                     if(ch.isLower()) {
0674                         m_undoableCursor.movePosition(QTextCursor::Start);
0675                         m_undoableCursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, bi.position() + i);
0676                         m_undoableCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, 1);
0677                         m_undoableCursor.insertText(QString(ch.toUpper()));
0678                     }
0679                 } else if(convertLowerCase) {
0680                     if(ch.isUpper()) {
0681                         m_undoableCursor.movePosition(QTextCursor::Start);
0682                         m_undoableCursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, bi.position() + i);
0683                         m_undoableCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, 1);
0684                         m_undoableCursor.insertText(QString(ch.toLower()));
0685                     }
0686                 }
0687             }
0688             if(isSentenceStart) {
0689                 if(isEndPunct)
0690                     *isSentenceStart = true;
0691                 else if(*isSentenceStart && !isSpace)
0692                     *isSentenceStart = false;
0693             }
0694             wordStart = isSpace || isEndPunct
0695                     || (ch != QChar('-') && ch != QChar('_') && ch != QChar('\'') && ch.isPunct());
0696         }
0697     }
0698     if(!testOnly)
0699         m_undoableCursor.endEditBlock();
0700 }
0701 
0702 void
0703 RichDocument::breakText(int minBreakLength)
0704 {
0705     Q_ASSERT(minBreakLength >= 0);
0706 
0707     if(length() <= minBreakLength)
0708         return;
0709 
0710     const double center = double(length()) / 2.;
0711     double brkD = std::numeric_limits<double>::infinity();
0712 
0713     m_undoableCursor.beginEditBlock();
0714     joinLines();
0715     const QString &text = firstBlock().text();
0716     for(int i = 0; i < text.length(); i++) {
0717         if(!text.at(i).isSpace())
0718             continue;
0719         const double nd = double(i) - center;
0720         if(qAbs(nd) < qAbs(brkD))
0721             brkD = nd;
0722     }
0723     if(qIsFinite(brkD)) {
0724         m_undoableCursor.movePosition(QTextCursor::Start);
0725         m_undoableCursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, int(center + brkD));
0726         m_undoableCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, 1);
0727         m_undoableCursor.insertText($("\n"));
0728     }
0729     m_undoableCursor.endEditBlock();
0730 }
0731 
0732 void
0733 RichDocument::replace(const QRegularExpression &search, const QString &replacement, bool replacementIsHtml)
0734 {
0735     if(!search.isValid()) {
0736         qWarning("RichDocument::replace(): invalid QRegularExpression object");
0737         return;
0738     }
0739 
0740     const QString copy(toPlainText());
0741     QRegularExpressionMatchIterator matches = search.globalMatch(copy);
0742     if(!matches.hasNext()) // no matches at all
0743         return;
0744 
0745     QTextCursor readCur(this);
0746     QVector<EditChange> cl;
0747 
0748     const int numCaptures = search.captureCount();
0749 
0750     QVector<REBackrefFragment> backRefFrags(numCaptures);
0751 
0752     // build the backreferences vector with offsets in the replacement string
0753     QVector<REStringCapture> backRefs;
0754     // build replacement string fragments, so html is properly preserved
0755     QVector<QSharedPointer<QTextDocumentFragment>> repFrags;
0756     {
0757         const int al = replacement.length();
0758         const QChar *ac = replacement.unicode();
0759         RichDocument repDoc;
0760         if(replacementIsHtml)
0761             repDoc.setHtml(replacement);
0762         else
0763             repDoc.setPlainText(replacement);
0764         QTextCursor repCur(&repDoc);
0765         for(int i = 0; i < al - 1; i++) {
0766             if(ac[i] == QLatin1Char('\\')) {
0767                 int no = ac[i + 1].digitValue();
0768                 if(no > 0 && no <= numCaptures) {
0769                     REStringCapture ref;
0770                     ref.pos = i;
0771                     ref.len = 2;
0772 
0773                     if(i < al - 2) {
0774                         const int secondDigit = ac[i + 2].digitValue();
0775                         if(secondDigit != -1) {
0776                             const int d = no * 10 + secondDigit;
0777                             if(d <= numCaptures) {
0778                                 no = d;
0779                                 ++ref.len;
0780                             }
0781                         }
0782                     }
0783 
0784                     ref.no = no;
0785                     backRefs.append(ref);
0786 
0787                     repCur.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, ref.pos - repCur.position());
0788                     repFrags.push_back(QSharedPointer<QTextDocumentFragment>(new QTextDocumentFragment(repCur)));
0789                     repCur.clearSelection();
0790                     repCur.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, ref.len);
0791                 }
0792             }
0793         }
0794         repCur.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
0795         repFrags.push_back(QSharedPointer<QTextDocumentFragment>(new QTextDocumentFragment(repCur)));
0796     }
0797 
0798     // iterate over matches
0799     while(matches.hasNext()) {
0800         QRegularExpressionMatch match = matches.next();
0801         const int matchStart = match.capturedStart();
0802         const int matchLen = match.capturedLength();
0803         int len;
0804         int repFrag = 0;
0805         // add the replacement string, with backreferences replaced
0806         for(const REStringCapture &backRef: qAsConst(backRefs)) {
0807             // part of the replacement string before the backreference
0808             if(!repFrags.at(repFrag)->isEmpty())
0809                 cl.push_back(EditChange(ReplaceFragment, matchStart, 0, repFrags.at(repFrag)));
0810             repFrag++;
0811 
0812             // backreference inside the replacement string
0813             if((len = match.capturedLength(backRef.no))) {
0814                 REBackrefFragment *brF = &backRefFrags[backRef.no - 1];
0815                 const int pos = match.capturedStart(backRef.no);
0816                 if(brF->frag.isNull() || pos != brF->pos || len != brF->len) {
0817                     readCur.movePosition(QTextCursor::Start);
0818                     readCur.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, pos);
0819                     readCur.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, len);
0820                     brF->frag.reset(new QTextDocumentFragment(readCur));
0821                 }
0822                 cl.push_back(EditChange(ReplaceFragment, matchStart, 0, brF->frag));
0823             }
0824         }
0825 
0826         // changes are applied in reverse, so match is overwritten last
0827         if(!repFrags.at(repFrag)->isEmpty()) {
0828             // last part of the replacement string
0829             cl.push_back(EditChange(ReplaceFragment, matchStart, matchLen, repFrags.at(repFrag)));
0830         } else if(matchLen) {
0831             // erase the matched part
0832             cl.push_back(EditChange(Delete, matchStart, matchLen));
0833         }
0834     }
0835 
0836     applyChanges(&cl);
0837 }
0838 
0839 void
0840 RichDocument::replace(int index, int len, const QString &replacement)
0841 {
0842     int oldLength = length();
0843 
0844     if(index < 0 || index >= oldLength)
0845         return;
0846 
0847     len = length(index, len);
0848 
0849     if(len == 0 && replacement.length() == 0)
0850         return;
0851 
0852     m_undoableCursor.beginEditBlock();
0853     m_undoableCursor.movePosition(QTextCursor::Start);
0854     m_undoableCursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, index);
0855     m_undoableCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, len);
0856     m_undoableCursor.insertText(replacement);
0857     m_undoableCursor.endEditBlock();
0858 }
0859 
0860 RichDOM *
0861 RichDocument::dom()
0862 {
0863     if(m_domDirty) {
0864         m_dom->update(this);
0865         m_domDirty = false;
0866     }
0867     return m_dom;
0868 }
0869 
0870 RichDOM::Node *
0871 RichDocument::nodeAt(quint32 pos, RichDOM::Node *root)
0872 {
0873     RichDOM::Node *n = (root ? root : dom()->m_root)->children;
0874     while(n) {
0875         if(pos >= n->nodeStart && pos < n->nodeEnd) {
0876             RichDOM::Node *s;
0877             if(n->children && (s = nodeAt(pos, n)))
0878                 return s;
0879             break;
0880         }
0881         n = n->next;
0882     }
0883     return n;
0884 }
0885 
0886 QString
0887 RichDocument::crumbAt(RichDOM::Node *n)
0888 {
0889     if(!n)
0890         return QString();
0891 
0892     QString crumb = n->cssSel();
0893     while(n->parent) {
0894         n = n->parent;
0895         crumb = n->cssSel() + $(" > ") + crumb;
0896     }
0897     return crumb;
0898 }