1/*
2 Copyright 2007-2008 by Robert Knight <robertknight@gmail.com>
3
4 This program is free software; you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or
7 (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program; if not, write to the Free Software
16 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301 USA.
18*/
19
20// Own
21#include "Filter.h"
22
23// System
24#include <iostream>
25
26// Qt
27#include <QAction>
28#include <QApplication>
29#include <QtAlgorithms>
30#include <QClipboard>
31#include <QString>
32#include <QTextStream>
33#include <QSharedData>
34#include <QFile>
35#include <QDesktopServices>
36#include <QUrl>
37
38// KDE
39//#include <KLocale>
40//#include <KRun>
41
42// Konsole
43#include "TerminalCharacterDecoder.h"
44#include "konsole_wcwidth.h"
45
46using namespace Konsole;
47
48FilterChain::~FilterChain()
49{
50 QMutableListIterator<Filter*> iter(*this);
51
52 while ( iter.hasNext() )
53 {
54 Filter* filter = iter.next();
55 iter.remove();
56 delete filter;
57 }
58}
59
60void FilterChain::addFilter(Filter* filter)
61{
62 append(filter);
63}
64void FilterChain::removeFilter(Filter* filter)
65{
66 removeAll(filter);
67}
68bool FilterChain::containsFilter(Filter* filter)
69{
70 return contains(filter);
71}
72void FilterChain::reset()
73{
74 QListIterator<Filter*> iter(*this);
75 while (iter.hasNext())
76 iter.next()->reset();
77}
78void FilterChain::setBuffer(const QString* buffer , const QList<int>* linePositions)
79{
80 QListIterator<Filter*> iter(*this);
81 while (iter.hasNext())
82 iter.next()->setBuffer(buffer,linePositions);
83}
84void FilterChain::process()
85{
86 QListIterator<Filter*> iter(*this);
87 while (iter.hasNext())
88 iter.next()->process();
89}
90void FilterChain::clear()
91{
92 QList<Filter*>::clear();
93}
94Filter::HotSpot* FilterChain::hotSpotAt(int line , int column) const
95{
96 QListIterator<Filter*> iter(*this);
97 while (iter.hasNext())
98 {
99 Filter* filter = iter.next();
100 Filter::HotSpot* spot = filter->hotSpotAt(line,column);
101 if ( spot != 0 )
102 {
103 return spot;
104 }
105 }
106
107 return 0;
108}
109
110QList<Filter::HotSpot*> FilterChain::hotSpots() const
111{
112 QList<Filter::HotSpot*> list;
113 QListIterator<Filter*> iter(*this);
114 while (iter.hasNext())
115 {
116 Filter* filter = iter.next();
117 list << filter->hotSpots();
118 }
119 return list;
120}
121//QList<Filter::HotSpot*> FilterChain::hotSpotsAtLine(int line) const;
122
123TerminalImageFilterChain::TerminalImageFilterChain()
124: _buffer(0)
125, _linePositions(0)
126{
127}
128
129TerminalImageFilterChain::~TerminalImageFilterChain()
130{
131 delete _buffer;
132 delete _linePositions;
133}
134
135void TerminalImageFilterChain::setImage(const Character* const image , int lines , int columns, const QVector<LineProperty>& lineProperties)
136{
137 if (empty())
138 return;
139
140 // reset all filters and hotspots
141 reset();
142
143 PlainTextDecoder decoder;
144 decoder.setTrailingWhitespace(false);
145
146 // setup new shared buffers for the filters to process on
147 QString* newBuffer = new QString();
148 QList<int>* newLinePositions = new QList<int>();
149 setBuffer( newBuffer , newLinePositions );
150
151 // free the old buffers
152 delete _buffer;
153 delete _linePositions;
154
155 _buffer = newBuffer;
156 _linePositions = newLinePositions;
157
158 QTextStream lineStream(_buffer);
159 decoder.begin(&lineStream);
160
161 for (int i=0 ; i < lines ; i++)
162 {
163 _linePositions->append(_buffer->length());
164 decoder.decodeLine(image + i*columns,columns,LINE_DEFAULT);
165
166 // pretend that each line ends with a newline character.
167 // this prevents a link that occurs at the end of one line
168 // being treated as part of a link that occurs at the start of the next line
169 //
170 // the downside is that links which are spread over more than one line are not
171 // highlighted.
172 //
173 // TODO - Use the "line wrapped" attribute associated with lines in a
174 // terminal image to avoid adding this imaginary character for wrapped
175 // lines
176 if ( !(lineProperties.value(i,LINE_DEFAULT) & LINE_WRAPPED) )
177 lineStream << QLatin1Char('\n');
178 }
179 decoder.end();
180}
181
182Filter::Filter() :
183_linePositions(0),
184_buffer(0)
185{
186}
187
188Filter::~Filter()
189{
190 qDeleteAll(_hotspotList);
191 _hotspotList.clear();
192}
193void Filter::reset()
194{
195 qDeleteAll(_hotspotList);
196 _hotspots.clear();
197 _hotspotList.clear();
198}
199
200void Filter::setBuffer(const QString* buffer , const QList<int>* linePositions)
201{
202 _buffer = buffer;
203 _linePositions = linePositions;
204}
205
206void Filter::getLineColumn(int position , int& startLine , int& startColumn)
207{
208 Q_ASSERT( _linePositions );
209 Q_ASSERT( _buffer );
210
211
212 for (int i = 0 ; i < _linePositions->count() ; i++)
213 {
214 int nextLine = 0;
215
216 if ( i == _linePositions->count()-1 )
217 nextLine = _buffer->length() + 1;
218 else
219 nextLine = _linePositions->value(i+1);
220
221 if ( _linePositions->value(i) <= position && position < nextLine )
222 {
223 startLine = i;
224 startColumn = string_width(buffer()->mid(_linePositions->value(i),position - _linePositions->value(i)).toStdWString());
225 return;
226 }
227 }
228}
229
230
231/*void Filter::addLine(const QString& text)
232{
233 _linePositions << _buffer.length();
234 _buffer.append(text);
235}*/
236
237const QString* Filter::buffer()
238{
239 return _buffer;
240}
241Filter::HotSpot::~HotSpot()
242{
243}
244void Filter::addHotSpot(HotSpot* spot)
245{
246 _hotspotList << spot;
247
248 for (int line = spot->startLine() ; line <= spot->endLine() ; line++)
249 {
250 _hotspots.insert(line,spot);
251 }
252}
253QList<Filter::HotSpot*> Filter::hotSpots() const
254{
255 return _hotspotList;
256}
257QList<Filter::HotSpot*> Filter::hotSpotsAtLine(int line) const
258{
259 return _hotspots.values(line);
260}
261
262Filter::HotSpot* Filter::hotSpotAt(int line , int column) const
263{
264 QListIterator<HotSpot*> spotIter(_hotspots.values(line));
265
266 while (spotIter.hasNext())
267 {
268 HotSpot* spot = spotIter.next();
269
270 if ( spot->startLine() == line && spot->startColumn() > column )
271 continue;
272 if ( spot->endLine() == line && spot->endColumn() < column )
273 continue;
274
275 return spot;
276 }
277
278 return 0;
279}
280
281Filter::HotSpot::HotSpot(int startLine , int startColumn , int endLine , int endColumn)
282 : _startLine(startLine)
283 , _startColumn(startColumn)
284 , _endLine(endLine)
285 , _endColumn(endColumn)
286 , _type(NotSpecified)
287{
288}
289QList<QAction*> Filter::HotSpot::actions()
290{
291 return QList<QAction*>();
292}
293int Filter::HotSpot::startLine() const
294{
295 return _startLine;
296}
297int Filter::HotSpot::endLine() const
298{
299 return _endLine;
300}
301int Filter::HotSpot::startColumn() const
302{
303 return _startColumn;
304}
305int Filter::HotSpot::endColumn() const
306{
307 return _endColumn;
308}
309Filter::HotSpot::Type Filter::HotSpot::type() const
310{
311 return _type;
312}
313void Filter::HotSpot::setType(Type type)
314{
315 _type = type;
316}
317
318RegExpFilter::RegExpFilter()
319{
320}
321
322RegExpFilter::HotSpot::HotSpot(int startLine,int startColumn,int endLine,int endColumn)
323 : Filter::HotSpot(startLine,startColumn,endLine,endColumn)
324{
325 setType(Marker);
326}
327
328void RegExpFilter::HotSpot::activate(const QString&)
329{
330}
331
332void RegExpFilter::HotSpot::setCapturedTexts(const QStringList& texts)
333{
334 _capturedTexts = texts;
335}
336QStringList RegExpFilter::HotSpot::capturedTexts() const
337{
338 return _capturedTexts;
339}
340
341void RegExpFilter::setRegExp(const QRegExp& regExp)
342{
343 _searchText = regExp;
344}
345QRegExp RegExpFilter::regExp() const
346{
347 return _searchText;
348}
349/*void RegExpFilter::reset(int)
350{
351 _buffer = QString();
352}*/
353void RegExpFilter::process()
354{
355 int pos = 0;
356 const QString* text = buffer();
357
358 Q_ASSERT( text );
359
360 // ignore any regular expressions which match an empty string.
361 // otherwise the while loop below will run indefinitely
362 static const QString emptyString;
363 if ( _searchText.exactMatch(emptyString) )
364 return;
365
366 while(pos >= 0)
367 {
368 pos = _searchText.indexIn(*text,pos);
369
370 if ( pos >= 0 )
371 {
372 int startLine = 0;
373 int endLine = 0;
374 int startColumn = 0;
375 int endColumn = 0;
376
377 getLineColumn(pos,startLine,startColumn);
378 getLineColumn(pos + _searchText.matchedLength(),endLine,endColumn);
379
380 RegExpFilter::HotSpot* spot = newHotSpot(startLine,startColumn,
381 endLine,endColumn);
382 spot->setCapturedTexts(_searchText.capturedTexts());
383
384 addHotSpot( spot );
385 pos += _searchText.matchedLength();
386
387 // if matchedLength == 0, the program will get stuck in an infinite loop
388 if ( _searchText.matchedLength() == 0 )
389 pos = -1;
390 }
391 }
392}
393
394RegExpFilter::HotSpot* RegExpFilter::newHotSpot(int startLine,int startColumn,
395 int endLine,int endColumn)
396{
397 return new RegExpFilter::HotSpot(startLine,startColumn,
398 endLine,endColumn);
399}
400RegExpFilter::HotSpot* UrlFilter::newHotSpot(int startLine,int startColumn,int endLine,
401 int endColumn)
402{
403 HotSpot *spot = new UrlFilter::HotSpot(startLine,startColumn,
404 endLine,endColumn);
405 connect(spot->getUrlObject(), &FilterObject::activated, this, &UrlFilter::activated);
406 return spot;
407}
408
409UrlFilter::HotSpot::HotSpot(int startLine,int startColumn,int endLine,int endColumn)
410: RegExpFilter::HotSpot(startLine,startColumn,endLine,endColumn)
411, _urlObject(new FilterObject(this))
412{
413 setType(Link);
414}
415
416UrlFilter::HotSpot::UrlType UrlFilter::HotSpot::urlType() const
417{
418 QString url = capturedTexts().constFirst();
419
420 if ( FullUrlRegExp.exactMatch(url) )
421 return StandardUrl;
422 else if ( EmailAddressRegExp.exactMatch(url) )
423 return Email;
424 else
425 return Unknown;
426}
427
428void UrlFilter::HotSpot::activate(const QString& actionName)
429{
430 QString url = capturedTexts().constFirst();
431
432 const UrlType kind = urlType();
433
434 if ( actionName == QLatin1String("copy-action") )
435 {
436 QApplication::clipboard()->setText(url);
437 return;
438 }
439
440 if ( actionName.isEmpty() || actionName == QLatin1String("open-action") || actionName == QLatin1String("click-action") )
441 {
442 if ( kind == StandardUrl )
443 {
444 // if the URL path does not include the protocol ( eg. "www.kde.org" ) then
445 // prepend http:// ( eg. "www.kde.org" --> "http://www.kde.org" )
446 if (!url.contains(QLatin1String("://")))
447 {
448 url.prepend(QLatin1String("http://"));
449 }
450 }
451 else if ( kind == Email )
452 {
453 url.prepend(QLatin1String("mailto:"));
454 }
455
456 _urlObject->emitActivated(QUrl(url, QUrl::StrictMode), actionName != QLatin1String("click-action"));
457 }
458}
459
460// Note: Altering these regular expressions can have a major effect on the performance of the filters
461// used for finding URLs in the text, especially if they are very general and could match very long
462// pieces of text.
463// Please be careful when altering them.
464
465//regexp matches:
466// full url:
467// protocolname:// or www. followed by anything other than whitespaces, <, >, ' or ", and ends before whitespaces, <, >, ', ", ], !, comma and dot
468const QRegExp UrlFilter::FullUrlRegExp(QLatin1String("(www\\.(?!\\.)|[a-z][a-z0-9+.-]*://)[^\\s<>'\"]+[^!,\\.\\s<>'\"\\]]"));
469// email address:
470// [word chars, dots or dashes]@[word chars, dots or dashes].[word chars]
471const QRegExp UrlFilter::EmailAddressRegExp(QLatin1String("\\b(\\w|\\.|-)+@(\\w|\\.|-)+\\.\\w+\\b"));
472
473// matches full url or email address
474const QRegExp UrlFilter::CompleteUrlRegExp(QLatin1Char('(')+FullUrlRegExp.pattern()+QLatin1Char('|')+
475 EmailAddressRegExp.pattern()+QLatin1Char(')'));
476
477UrlFilter::UrlFilter()
478{
479 setRegExp( CompleteUrlRegExp );
480}
481
482UrlFilter::HotSpot::~HotSpot()
483{
484 delete _urlObject;
485}
486
487void FilterObject::emitActivated(const QUrl& url, bool fromContextMenu)
488{
489 emit activated(url, fromContextMenu);
490}
491
492void FilterObject::activate()
493{
494 _filter->activate(sender()->objectName());
495}
496
497FilterObject* UrlFilter::HotSpot::getUrlObject() const
498{
499 return _urlObject;
500}
501
502QList<QAction*> UrlFilter::HotSpot::actions()
503{
504 QList<QAction*> list;
505
506 const UrlType kind = urlType();
507
508 QAction* openAction = new QAction(_urlObject);
509 QAction* copyAction = new QAction(_urlObject);;
510
511 Q_ASSERT( kind == StandardUrl || kind == Email );
512
513 if ( kind == StandardUrl )
514 {
515 openAction->setText(QObject::tr("Open Link"));
516 copyAction->setText(QObject::tr("Copy Link Address"));
517 }
518 else if ( kind == Email )
519 {
520 openAction->setText(QObject::tr("Send Email To..."));
521 copyAction->setText(QObject::tr("Copy Email Address"));
522 }
523
524 // object names are set here so that the hotspot performs the
525 // correct action when activated() is called with the triggered
526 // action passed as a parameter.
527 openAction->setObjectName( QLatin1String("open-action" ));
528 copyAction->setObjectName( QLatin1String("copy-action" ));
529
530 QObject::connect( openAction , &QAction::triggered , _urlObject , &FilterObject::activate );
531 QObject::connect( copyAction , &QAction::triggered , _urlObject , &FilterObject::activate );
532
533 list << openAction;
534 list << copyAction;
535
536 return list;
537}
538
539//#include "Filter.moc"
540