File indexing completed on 2024-05-19 05:28:15

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 &regExp)
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"