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 }