File indexing completed on 2025-03-16 08:11:24
0001 /* 0002 SPDX-FileCopyrightText: 2007-2008 Robert Knight <robertknight@gmail.com> 0003 0004 SPDX-License-Identifier: GPL-2.0-or-later 0005 0006 This program is distributed in the hope that it will be useful, 0007 but WITHOUT ANY WARRANTY; without even the implied warranty of 0008 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 0009 GNU General Public License for more details. 0010 0011 You should have received a copy of the GNU General Public License 0012 along with this program; if not, write to the Free Software 0013 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 0014 02110-1301 USA. 0015 */ 0016 0017 // Own 0018 #include "Filter.h" 0019 0020 // System 0021 #include <iostream> 0022 0023 // Qt 0024 #include <QAction> 0025 #include <QApplication> 0026 #include <QClipboard> 0027 #include <QDesktopServices> 0028 #include <QFile> 0029 #include <QSharedData> 0030 #include <QString> 0031 #include <QTextStream> 0032 #include <QUrl> 0033 #include <QtAlgorithms> 0034 0035 // KDE 0036 // #include <KLocale> 0037 // #include <KRun> 0038 0039 // Konsole 0040 #include "TerminalCharacterDecoder.h" 0041 #include "konsole_wcwidth.h" 0042 0043 using namespace Konsole; 0044 0045 FilterChain::~FilterChain() = default; 0046 0047 void FilterChain::addFilter(std::unique_ptr<Filter> &&filter) 0048 { 0049 push_back(std::move(filter)); 0050 } 0051 0052 void FilterChain::removeFilter(Filter *filter) 0053 { 0054 std::erase_if(*this, [filter](const auto &f) { 0055 return f.get() == filter; 0056 }); 0057 } 0058 0059 bool FilterChain::containsFilter(Filter *filter) 0060 { 0061 return std::find_if(this->cbegin(), 0062 this->cend(), 0063 [filter](const auto &f) { 0064 return f.get() == filter; 0065 }) 0066 != this->cend(); 0067 } 0068 0069 void FilterChain::reset() 0070 { 0071 for (const auto &filter : *this) { 0072 filter->reset(); 0073 } 0074 } 0075 0076 void FilterChain::setBuffer(const QString *buffer, const QList<int> *linePositions) 0077 { 0078 for (const auto &filter : *this) { 0079 filter->setBuffer(buffer, linePositions); 0080 } 0081 } 0082 0083 void FilterChain::process() 0084 { 0085 for (const auto &filter : *this) { 0086 filter->process(); 0087 } 0088 } 0089 0090 Filter::HotSpot *FilterChain::hotSpotAt(int line, int column) const 0091 { 0092 for (const auto &filter : *this) { 0093 Filter::HotSpot *spot = filter->hotSpotAt(line, column); 0094 if (spot) { 0095 return spot; 0096 } 0097 } 0098 0099 return nullptr; 0100 } 0101 0102 QList<Filter::HotSpot *> FilterChain::hotSpots() const 0103 { 0104 QList<Filter::HotSpot *> list; 0105 for (const auto &filter : *this) { 0106 for (const auto &hotSpot : filter->hotSpots()) { 0107 list << hotSpot.get(); 0108 } 0109 } 0110 return list; 0111 } 0112 // QList<Filter::HotSpot*> FilterChain::hotSpotsAtLine(int line) const; 0113 0114 TerminalImageFilterChain::TerminalImageFilterChain() 0115 : _buffer(nullptr) 0116 , _linePositions(nullptr) 0117 { 0118 } 0119 0120 TerminalImageFilterChain::~TerminalImageFilterChain() 0121 { 0122 delete _buffer; 0123 delete _linePositions; 0124 } 0125 0126 void TerminalImageFilterChain::setImage(std::span<const Character> image, int lines, int columns, const QVector<LineProperty> &lineProperties) 0127 { 0128 if (empty()) 0129 return; 0130 0131 // reset all filters and hotspots 0132 reset(); 0133 0134 PlainTextDecoder decoder; 0135 decoder.setTrailingWhitespace(false); 0136 0137 // setup new shared buffers for the filters to process on 0138 QString *newBuffer = new QString(); 0139 QList<int> *newLinePositions = new QList<int>(); 0140 setBuffer(newBuffer, newLinePositions); 0141 0142 // free the old buffers 0143 delete _buffer; 0144 delete _linePositions; 0145 0146 _buffer = newBuffer; 0147 _linePositions = newLinePositions; 0148 0149 QTextStream lineStream(_buffer); 0150 decoder.begin(&lineStream); 0151 0152 for (int i = 0; i < lines; i++) { 0153 _linePositions->append(_buffer->length()); 0154 decoder.decodeLine(image.subspan(i * columns, columns), LINE_DEFAULT); 0155 0156 // pretend that each line ends with a newline character. 0157 // this prevents a link that occurs at the end of one line 0158 // being treated as part of a link that occurs at the start of the next line 0159 // 0160 // the downside is that links which are spread over more than one line are not 0161 // highlighted. 0162 // 0163 // TODO - Use the "line wrapped" attribute associated with lines in a 0164 // terminal image to avoid adding this imaginary character for wrapped 0165 // lines 0166 if (!(lineProperties.value(i, LINE_DEFAULT) & LINE_WRAPPED)) 0167 lineStream << QLatin1Char('\n'); 0168 } 0169 decoder.end(); 0170 } 0171 0172 Filter::Filter() 0173 : _linePositions(nullptr) 0174 , _buffer(nullptr) 0175 { 0176 } 0177 0178 Filter::~Filter() 0179 { 0180 _hotspotList.clear(); 0181 } 0182 void Filter::reset() 0183 { 0184 _hotspots.clear(); 0185 _hotspotList.clear(); 0186 } 0187 0188 void Filter::setBuffer(const QString *buffer, const QList<int> *linePositions) 0189 { 0190 _buffer = buffer; 0191 _linePositions = linePositions; 0192 } 0193 0194 void Filter::getLineColumn(int position, int &startLine, int &startColumn) 0195 { 0196 Q_ASSERT(_linePositions); 0197 Q_ASSERT(_buffer); 0198 0199 for (int i = 0; i < _linePositions->count(); i++) { 0200 int nextLine = 0; 0201 0202 if (i == _linePositions->count() - 1) 0203 nextLine = _buffer->length() + 1; 0204 else 0205 nextLine = _linePositions->value(i + 1); 0206 0207 if (_linePositions->value(i) <= position && position < nextLine) { 0208 startLine = i; 0209 startColumn = string_width(buffer()->mid(_linePositions->value(i), position - _linePositions->value(i))); 0210 return; 0211 } 0212 } 0213 } 0214 0215 /*void Filter::addLine(const QString& text) 0216 { 0217 _linePositions << _buffer.length(); 0218 _buffer.append(text); 0219 }*/ 0220 0221 const QString *Filter::buffer() 0222 { 0223 return _buffer; 0224 } 0225 0226 Filter::HotSpot::~HotSpot() = default; 0227 0228 void Filter::addHotSpot(std::unique_ptr<HotSpot> &&spot) 0229 { 0230 _hotspotList.push_back(std::move(spot)); 0231 0232 for (int line = spot->startLine(); line <= spot->endLine(); line++) { 0233 _hotspots.insert({line, std::move(spot)}); 0234 } 0235 } 0236 0237 const std::vector<std::unique_ptr<Filter::HotSpot>> &Filter::hotSpots() const 0238 { 0239 return _hotspotList; 0240 } 0241 0242 QList<Filter::HotSpot *> Filter::hotSpotsAtLine(int line) const 0243 { 0244 QList<Filter::HotSpot *> filters; 0245 for (auto it = _hotspots.find(line); it != _hotspots.cend(); it++) { 0246 filters.push_back(it->second.get()); 0247 } 0248 return filters; 0249 } 0250 0251 Filter::HotSpot *Filter::hotSpotAt(int line, int column) const 0252 { 0253 const auto hotspots = hotSpotsAtLine(line); 0254 0255 for (const auto spot : hotspots) { 0256 if (spot->startLine() == line && spot->startColumn() > column) 0257 continue; 0258 if (spot->endLine() == line && spot->endColumn() < column) 0259 continue; 0260 0261 return spot; 0262 } 0263 0264 return nullptr; 0265 } 0266 0267 Filter::HotSpot::HotSpot(int startLine, int startColumn, int endLine, int endColumn) 0268 : _startLine(startLine) 0269 , _startColumn(startColumn) 0270 , _endLine(endLine) 0271 , _endColumn(endColumn) 0272 , _type(NotSpecified) 0273 { 0274 } 0275 QList<QAction *> Filter::HotSpot::actions() 0276 { 0277 return QList<QAction *>(); 0278 } 0279 int Filter::HotSpot::startLine() const 0280 { 0281 return _startLine; 0282 } 0283 int Filter::HotSpot::endLine() const 0284 { 0285 return _endLine; 0286 } 0287 int Filter::HotSpot::startColumn() const 0288 { 0289 return _startColumn; 0290 } 0291 int Filter::HotSpot::endColumn() const 0292 { 0293 return _endColumn; 0294 } 0295 Filter::HotSpot::Type Filter::HotSpot::type() const 0296 { 0297 return _type; 0298 } 0299 void Filter::HotSpot::setType(Type type) 0300 { 0301 _type = type; 0302 } 0303 0304 RegExpFilter::RegExpFilter() 0305 { 0306 } 0307 0308 RegExpFilter::HotSpot::HotSpot(int startLine, int startColumn, int endLine, int endColumn) 0309 : Filter::HotSpot(startLine, startColumn, endLine, endColumn) 0310 { 0311 setType(Marker); 0312 } 0313 0314 void RegExpFilter::HotSpot::activate(const QString &) 0315 { 0316 } 0317 0318 void RegExpFilter::HotSpot::setCapturedTexts(const QStringList &texts) 0319 { 0320 _capturedTexts = texts; 0321 } 0322 QStringList RegExpFilter::HotSpot::capturedTexts() const 0323 { 0324 return _capturedTexts; 0325 } 0326 0327 void RegExpFilter::setRegExp(const QRegExp ®Exp) 0328 { 0329 _searchText = regExp; 0330 } 0331 QRegExp RegExpFilter::regExp() const 0332 { 0333 return _searchText; 0334 } 0335 /*void RegExpFilter::reset(int) 0336 { 0337 _buffer = QString(); 0338 }*/ 0339 void RegExpFilter::process() 0340 { 0341 int pos = 0; 0342 const QString *text = buffer(); 0343 0344 Q_ASSERT(text); 0345 0346 // ignore any regular expressions which match an empty string. 0347 // otherwise the while loop below will run indefinitely 0348 static const QString emptyString; 0349 if (_searchText.exactMatch(emptyString)) 0350 return; 0351 0352 while (pos >= 0) { 0353 pos = _searchText.indexIn(*text, pos); 0354 0355 if (pos >= 0) { 0356 int startLine = 0; 0357 int endLine = 0; 0358 int startColumn = 0; 0359 int endColumn = 0; 0360 0361 getLineColumn(pos, startLine, startColumn); 0362 getLineColumn(pos + _searchText.matchedLength(), endLine, endColumn); 0363 0364 auto spot = newHotSpot(startLine, startColumn, endLine, endColumn); 0365 spot->setCapturedTexts(_searchText.capturedTexts()); 0366 0367 addHotSpot(std::move(spot)); 0368 pos += _searchText.matchedLength(); 0369 0370 // if matchedLength == 0, the program will get stuck in an infinite loop 0371 if (_searchText.matchedLength() == 0) 0372 pos = -1; 0373 } 0374 } 0375 } 0376 0377 std::unique_ptr<RegExpFilter::HotSpot> RegExpFilter::newHotSpot(int startLine, int startColumn, int endLine, int endColumn) 0378 { 0379 return std::make_unique<RegExpFilter::HotSpot>(startLine, startColumn, endLine, endColumn); 0380 } 0381 0382 std::unique_ptr<RegExpFilter::HotSpot> UrlFilter::newHotSpot(int startLine, int startColumn, int endLine, int endColumn) 0383 { 0384 auto spot = std::make_unique<UrlFilter::HotSpot>(startLine, startColumn, endLine, endColumn); 0385 connect(spot->getUrlObject(), &FilterObject::activated, this, &UrlFilter::activated); 0386 return spot; 0387 } 0388 0389 UrlFilter::HotSpot::HotSpot(int startLine, int startColumn, int endLine, int endColumn) 0390 : RegExpFilter::HotSpot(startLine, startColumn, endLine, endColumn) 0391 , _urlObject(new FilterObject(this)) 0392 { 0393 setType(Link); 0394 } 0395 0396 UrlFilter::HotSpot::UrlType UrlFilter::HotSpot::urlType() const 0397 { 0398 QString url = capturedTexts().constFirst(); 0399 0400 if (FullUrlRegExp.exactMatch(url)) 0401 return StandardUrl; 0402 else if (EmailAddressRegExp.exactMatch(url)) 0403 return Email; 0404 else 0405 return Unknown; 0406 } 0407 0408 void UrlFilter::HotSpot::activate(const QString &actionName) 0409 { 0410 QString url = capturedTexts().constFirst(); 0411 0412 const UrlType kind = urlType(); 0413 0414 if (actionName == QLatin1String("copy-action")) { 0415 QApplication::clipboard()->setText(url); 0416 return; 0417 } 0418 0419 if (actionName.isEmpty() || actionName == QLatin1String("open-action") || actionName == QLatin1String("click-action")) { 0420 if (kind == StandardUrl) { 0421 // if the URL path does not include the protocol ( eg. "www.kde.org" ) then 0422 // prepend http:// ( eg. "www.kde.org" --> "http://www.kde.org" ) 0423 if (!url.contains(QLatin1String("://"))) { 0424 url.prepend(QLatin1String("http://")); 0425 } 0426 } else if (kind == Email) { 0427 url.prepend(QLatin1String("mailto:")); 0428 } 0429 0430 _urlObject->emitActivated(QUrl(url, QUrl::StrictMode), actionName != QLatin1String("click-action")); 0431 } 0432 } 0433 0434 // Note: Altering these regular expressions can have a major effect on the performance of the filters 0435 // used for finding URLs in the text, especially if they are very general and could match very long 0436 // pieces of text. 0437 // Please be careful when altering them. 0438 0439 // regexp matches: 0440 // full url: 0441 // protocolname:// or www. followed by anything other than whitespaces, <, >, ' or ", and ends before whitespaces, <, >, ', ", ], !, comma and dot 0442 const QRegExp UrlFilter::FullUrlRegExp(QLatin1String("(www\\.(?!\\.)|[a-z][a-z0-9+.-]*://)[^\\s<>'\"]+[^!,\\.\\s<>'\"\\]]")); 0443 // email address: 0444 // [word chars, dots or dashes]@[word chars, dots or dashes].[word chars] 0445 const QRegExp UrlFilter::EmailAddressRegExp(QLatin1String("\\b(\\w|\\.|-)+@(\\w|\\.|-)+\\.\\w+\\b")); 0446 0447 // matches full url or email address 0448 const QRegExp UrlFilter::CompleteUrlRegExp(QLatin1Char('(') + FullUrlRegExp.pattern() + QLatin1Char('|') + EmailAddressRegExp.pattern() + QLatin1Char(')')); 0449 0450 UrlFilter::UrlFilter() 0451 { 0452 setRegExp(CompleteUrlRegExp); 0453 } 0454 0455 UrlFilter::HotSpot::~HotSpot() 0456 { 0457 delete _urlObject; 0458 } 0459 0460 void FilterObject::emitActivated(const QUrl &url, bool fromContextMenu) 0461 { 0462 Q_EMIT activated(url, fromContextMenu); 0463 } 0464 0465 void FilterObject::activate() 0466 { 0467 _filter->activate(sender()->objectName()); 0468 } 0469 0470 FilterObject *UrlFilter::HotSpot::getUrlObject() const 0471 { 0472 return _urlObject; 0473 } 0474 0475 QList<QAction *> UrlFilter::HotSpot::actions() 0476 { 0477 QList<QAction *> list; 0478 0479 const UrlType kind = urlType(); 0480 0481 QAction *openAction = new QAction(_urlObject); 0482 QAction *copyAction = new QAction(_urlObject); 0483 ; 0484 0485 Q_ASSERT(kind == StandardUrl || kind == Email); 0486 0487 if (kind == StandardUrl) { 0488 openAction->setText(QObject::tr("Open Link")); 0489 copyAction->setText(QObject::tr("Copy Link Address")); 0490 } else if (kind == Email) { 0491 openAction->setText(QObject::tr("Send Email To...")); 0492 copyAction->setText(QObject::tr("Copy Email Address")); 0493 } 0494 0495 // object names are set here so that the hotspot performs the 0496 // correct action when activated() is called with the triggered 0497 // action passed as a parameter. 0498 openAction->setObjectName(QLatin1String("open-action")); 0499 copyAction->setObjectName(QLatin1String("copy-action")); 0500 0501 QObject::connect(openAction, &QAction::triggered, _urlObject, &FilterObject::activate); 0502 QObject::connect(copyAction, &QAction::triggered, _urlObject, &FilterObject::activate); 0503 0504 list << openAction; 0505 list << copyAction; 0506 0507 return list; 0508 } 0509 0510 // #include "Filter.moc"