Filter.cpp 15.7 KB
Newer Older
1
/*
2
    Copyright 2007-2008 by Robert Knight <robertknight@gmail.com>
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
    02110-1301  USA.
*/

20
21
22
// Own
#include "Filter.h"

23
#include "konsoledebug.h"
Tomaz  Canabrava's avatar
Tomaz Canabrava committed
24
#include <algorithm>
25

26
// Qt
27
28
#include <QAction>
#include <QApplication>
29
30
31
32
33
34
#include <QClipboard>
#include <QDir>
#include <QMimeDatabase>
#include <QString>
#include <QTextStream>
#include <QUrl>
35
36

// KDE
37
#include <KLocalizedString>
38
39
40
#include <KRun>

// Konsole
41
#include "Session.h"
42
43
#include "TerminalCharacterDecoder.h"

44
45
using namespace Konsole;

46
47
FilterChain::~FilterChain()
{
48
    qDeleteAll(_filters);
49
50
}

Kurt Hindenburg's avatar
Kurt Hindenburg committed
51
void FilterChain::addFilter(Filter *filter)
52
{
53
    _filters.append(filter);
54
}
Kurt Hindenburg's avatar
Kurt Hindenburg committed
55
56

void FilterChain::removeFilter(Filter *filter)
57
{
58
    _filters.removeAll(filter);
59
}
Kurt Hindenburg's avatar
Kurt Hindenburg committed
60

61
62
void FilterChain::reset()
{
63
64
    for(auto *filter : _filters) {
        filter->reset();
Kurt Hindenburg's avatar
Kurt Hindenburg committed
65
    }
66
}
Kurt Hindenburg's avatar
Kurt Hindenburg committed
67
68

void FilterChain::setBuffer(const QString *buffer, const QList<int> *linePositions)
69
{
70
71
    for(auto *filter : _filters) {
        filter->setBuffer(buffer, linePositions);
Kurt Hindenburg's avatar
Kurt Hindenburg committed
72
    }
73
}
Kurt Hindenburg's avatar
Kurt Hindenburg committed
74

75
void FilterChain::process()
76
{
77
78
    for( auto *filter : _filters) {
        filter->process();
Kurt Hindenburg's avatar
Kurt Hindenburg committed
79
    }
80
}
Kurt Hindenburg's avatar
Kurt Hindenburg committed
81

82
83
void FilterChain::clear()
{
84
    _filters.clear();
85
}
Kurt Hindenburg's avatar
Kurt Hindenburg committed
86

87
QSharedPointer<Filter::HotSpot> FilterChain::hotSpotAt(int line, int column) const
88
{
89
    for(auto *filter : _filters) {
90
        QSharedPointer<Filter::HotSpot> spot = filter->hotSpotAt(line, column);
Kurt Hindenburg's avatar
Kurt Hindenburg committed
91
        if (spot != nullptr) {
92
           return spot;
93
94
        }
    }
Kurt Hindenburg's avatar
Kurt Hindenburg committed
95
    return nullptr;
96
97
}

98
QList<QSharedPointer<Filter::HotSpot>> FilterChain::hotSpots() const
99
{
100
    QList<QSharedPointer<Filter::HotSpot>> list;
101
102
    for (auto *filter : _filters) {
        list.append(filter->hotSpots());
Tomaz  Canabrava's avatar
Tomaz Canabrava committed
103
   }
104
105
    return list;
}
106

Kurt Hindenburg's avatar
Kurt Hindenburg committed
107
TerminalImageFilterChain::TerminalImageFilterChain() :
Kurt Hindenburg's avatar
Kurt Hindenburg committed
108
109
    _buffer(nullptr),
    _linePositions(nullptr)
110
111
112
{
}

113
TerminalImageFilterChain::~TerminalImageFilterChain() = default;
114

Kurt Hindenburg's avatar
Kurt Hindenburg committed
115
116
void TerminalImageFilterChain::setImage(const Character * const image, int lines, int columns,
                                        const QVector<LineProperty> &lineProperties)
117
{
118
    if (_filters.empty()) {
119
        return;
Kurt Hindenburg's avatar
Kurt Hindenburg committed
120
    }
121

122
123
124
    // reset all filters and hotspots
    reset();

125
    PlainTextDecoder decoder;
126
127
    decoder.setLeadingWhitespace(true);
    decoder.setTrailingWhitespace(true);
128

129
    // setup new shared buffers for the filters to process on
130
131
    _buffer.reset(new QString());
    _linePositions.reset(new QList<int>());
132

133
    setBuffer(_buffer.get(), _linePositions.get());
134

135
    QTextStream lineStream(_buffer.get());
136
137
    decoder.begin(&lineStream);

Kurt Hindenburg's avatar
Kurt Hindenburg committed
138
    for (int i = 0; i < lines; i++) {
139
        _linePositions->append(_buffer->length());
Kurt Hindenburg's avatar
Kurt Hindenburg committed
140
        decoder.decodeLine(image + i * columns, columns, LINE_DEFAULT);
141
142
143
144
145
146

        // pretend that each line ends with a newline character.
        // this prevents a link that occurs at the end of one line
        // being treated as part of a link that occurs at the start of the next line
        //
        // the downside is that links which are spread over more than one line are not
Kurt Hindenburg's avatar
Kurt Hindenburg committed
147
        // highlighted.
148
149
150
151
        //
        // TODO - Use the "line wrapped" attribute associated with lines in a
        // terminal image to avoid adding this imaginary character for wrapped
        // lines
Kurt Hindenburg's avatar
Kurt Hindenburg committed
152
        if ((lineProperties.value(i, LINE_DEFAULT) & LINE_WRAPPED) == 0) {
153
            lineStream << QLatin1Char('\n');
Kurt Hindenburg's avatar
Kurt Hindenburg committed
154
        }
155
    }
156
    decoder.end();
157
158
}

Kurt Hindenburg's avatar
Kurt Hindenburg committed
159
Filter::Filter() :
Kurt Hindenburg's avatar
Kurt Hindenburg committed
160
161
    _linePositions(nullptr),
    _buffer(nullptr)
162
163
164
{
}

165
166
Filter::~Filter()
{
David Hallas's avatar
David Hallas committed
167
    reset();
168
}
Kurt Hindenburg's avatar
Kurt Hindenburg committed
169

170
171
172
173
void Filter::reset()
{
    _hotspots.clear();
    _hotspotList.clear();
174
175
}

Kurt Hindenburg's avatar
Kurt Hindenburg committed
176
void Filter::setBuffer(const QString *buffer, const QList<int> *linePositions)
177
178
179
{
    _buffer = buffer;
    _linePositions = linePositions;
180
181
}

182
std::pair<int, int> Filter::getLineColumn(int position)
183
{
Kurt Hindenburg's avatar
Kurt Hindenburg committed
184
185
    Q_ASSERT(_linePositions);
    Q_ASSERT(_buffer);
186

Kurt Hindenburg's avatar
Kurt Hindenburg committed
187
    for (int i = 0; i < _linePositions->count(); i++) {
188
189
190
        const int nextLine = i == _linePositions->count() - 1
            ? _buffer->length() + 1
            : _linePositions->value(i + 1);
191

Kurt Hindenburg's avatar
Kurt Hindenburg committed
192
        if (_linePositions->value(i) <= position && position < nextLine) {
193
194
            return std::make_pair(i,  Character::stringWidth(buffer()->mid(_linePositions->value(i),
                                                     position - _linePositions->value(i))));
195
196
        }
    }
197
    return std::make_pair(-1, -1);
198
}
199

Kurt Hindenburg's avatar
Kurt Hindenburg committed
200
const QString *Filter::buffer()
201
202
203
{
    return _buffer;
}
Kurt Hindenburg's avatar
Kurt Hindenburg committed
204

205
206
Filter::HotSpot::~HotSpot() = default;

207
void Filter::addHotSpot(QSharedPointer<HotSpot> spot)
208
209
210
{
    _hotspotList << spot;

Kurt Hindenburg's avatar
Kurt Hindenburg committed
211
    for (int line = spot->startLine(); line <= spot->endLine(); line++) {
Kurt Hindenburg's avatar
Kurt Hindenburg committed
212
213
        _hotspots.insert(line, spot);
    }
214
}
Kurt Hindenburg's avatar
Kurt Hindenburg committed
215

216
QList<QSharedPointer<Filter::HotSpot>> Filter::hotSpots() const
217
218
219
{
    return _hotspotList;
}
Kurt Hindenburg's avatar
Kurt Hindenburg committed
220

221
QSharedPointer<Filter::HotSpot> Filter::hotSpotAt(int line, int column) const
222
{
223
    const auto hotspots = _hotspots.values(line);
224

225
    for (auto &spot : hotspots) {
Kurt Hindenburg's avatar
Kurt Hindenburg committed
226
        if (spot->startLine() == line && spot->startColumn() > column) {
227
            continue;
Kurt Hindenburg's avatar
Kurt Hindenburg committed
228
229
        }
        if (spot->endLine() == line && spot->endColumn() < column) {
230
            continue;
Kurt Hindenburg's avatar
Kurt Hindenburg committed
231
        }
232

233
234
235
        return spot;
    }

Kurt Hindenburg's avatar
Kurt Hindenburg committed
236
    return nullptr;
237
238
}

Kurt Hindenburg's avatar
Kurt Hindenburg committed
239
240
241
242
243
244
Filter::HotSpot::HotSpot(int startLine, int startColumn, int endLine, int endColumn) :
    _startLine(startLine),
    _startColumn(startColumn),
    _endLine(endLine),
    _endColumn(endColumn),
    _type(NotSpecified)
245
246
{
}
Kurt Hindenburg's avatar
Kurt Hindenburg committed
247

Tomaz  Canabrava's avatar
Tomaz Canabrava committed
248
QList<QAction *> Filter::HotSpot::actions()
249
{
Tomaz Canabrava's avatar
Tomaz Canabrava committed
250
    return {};
251
}
Kurt Hindenburg's avatar
Kurt Hindenburg committed
252

253
254
255
256
int Filter::HotSpot::startLine() const
{
    return _startLine;
}
Kurt Hindenburg's avatar
Kurt Hindenburg committed
257

258
259
260
261
int Filter::HotSpot::endLine() const
{
    return _endLine;
}
Kurt Hindenburg's avatar
Kurt Hindenburg committed
262

263
264
265
266
int Filter::HotSpot::startColumn() const
{
    return _startColumn;
}
Kurt Hindenburg's avatar
Kurt Hindenburg committed
267

268
269
270
271
int Filter::HotSpot::endColumn() const
{
    return _endColumn;
}
Kurt Hindenburg's avatar
Kurt Hindenburg committed
272

273
274
275
276
Filter::HotSpot::Type Filter::HotSpot::type() const
{
    return _type;
}
Kurt Hindenburg's avatar
Kurt Hindenburg committed
277

278
279
280
281
282
void Filter::HotSpot::setType(Type type)
{
    _type = type;
}

Kurt Hindenburg's avatar
Kurt Hindenburg committed
283
284
RegExpFilter::RegExpFilter() :
    _searchText(QRegularExpression())
285
286
287
{
}

Kurt Hindenburg's avatar
Kurt Hindenburg committed
288
289
290
291
RegExpFilter::HotSpot::HotSpot(int startLine, int startColumn, int endLine, int endColumn,
                               const QStringList &capturedTexts) :
    Filter::HotSpot(startLine, startColumn, endLine, endColumn),
    _capturedTexts(capturedTexts)
292
293
294
{
    setType(Marker);
}
295

Kurt Hindenburg's avatar
Kurt Hindenburg committed
296
void RegExpFilter::HotSpot::activate(QObject *)
297
298
299
300
301
302
303
304
{
}

QStringList RegExpFilter::HotSpot::capturedTexts() const
{
    return _capturedTexts;
}

305
void RegExpFilter::setRegExp(const QRegularExpression &regExp)
306
{
307
    _searchText = regExp;
308
    _searchText.optimize();
309
}
Kurt Hindenburg's avatar
Kurt Hindenburg committed
310

311
QRegularExpression RegExpFilter::regExp() const
312
313
314
{
    return _searchText;
}
Kurt Hindenburg's avatar
Kurt Hindenburg committed
315

316
317
void RegExpFilter::process()
{
Kurt Hindenburg's avatar
Kurt Hindenburg committed
318
    const QString *text = buffer();
319

Kurt Hindenburg's avatar
Kurt Hindenburg committed
320
    Q_ASSERT(text);
321

322
    if (!_searchText.isValid() || _searchText.pattern().isEmpty()) {
323
        return;
324
    }
325

326
327
328
    QRegularExpressionMatchIterator iterator(_searchText.globalMatch(*text));
    while (iterator.hasNext()) {
        QRegularExpressionMatch match(iterator.next());
Tomaz  Canabrava's avatar
Tomaz Canabrava committed
329
330
331
332
333
334
335
336
337
        std::pair<int, int> start = getLineColumn(match.capturedStart());
        std::pair<int, int> end = getLineColumn(match.capturedEnd());

        QSharedPointer<Filter::HotSpot> spot(
            newHotSpot(start.first, start.second,
                       end.first, end.second,
                       match.capturedTexts()
            )
        );
338

339
        if (spot == nullptr) {
340
            continue;
341
        }
342
343

        addHotSpot(spot);
Kurt Hindenburg's avatar
Kurt Hindenburg committed
344
    }
345
346
}

347
QSharedPointer<Filter::HotSpot> RegExpFilter::newHotSpot(int startLine, int startColumn, int endLine,
Kurt Hindenburg's avatar
Kurt Hindenburg committed
348
                                                int endColumn, const QStringList &capturedTexts)
349
{
350
351
    return QSharedPointer<Filter::HotSpot>(new RegExpFilter::HotSpot(startLine, startColumn,
                                     endLine, endColumn, capturedTexts));
352
}
Kurt Hindenburg's avatar
Kurt Hindenburg committed
353

354
QSharedPointer<Filter::HotSpot> UrlFilter::newHotSpot(int startLine, int startColumn, int endLine,
Kurt Hindenburg's avatar
Kurt Hindenburg committed
355
                                             int endColumn, const QStringList &capturedTexts)
356
{
357
358
    return QSharedPointer<Filter::HotSpot>(new UrlFilter::HotSpot(startLine, startColumn,
                                  endLine, endColumn, capturedTexts));
359
}
Kurt Hindenburg's avatar
Kurt Hindenburg committed
360

361
UrlFilter::HotSpot::HotSpot(int startLine, int startColumn, int endLine, int endColumn,
Kurt Hindenburg's avatar
Kurt Hindenburg committed
362
                            const QStringList &capturedTexts) :
363
    RegExpFilter::HotSpot(startLine, startColumn, endLine, endColumn, capturedTexts)
364
{
365
    setType(Link);
366
}
Kurt Hindenburg's avatar
Kurt Hindenburg committed
367

368
UrlFilter::HotSpot::UrlType UrlFilter::HotSpot::urlType() const
369
{
370
    const QString url = capturedTexts().at(0);
371
372
373
    return FullUrlRegExp.match(url).hasMatch() ? StandardUrl
         : EmailAddressRegExp.match(url).hasMatch() ? Email
         : Unknown;
374
375
}

Kurt Hindenburg's avatar
Kurt Hindenburg committed
376
void UrlFilter::HotSpot::activate(QObject *object)
377
{
378
    QString url = capturedTexts().at(0);
379

380
381
    const UrlType kind = urlType();

Kurt Hindenburg's avatar
Kurt Hindenburg committed
382
    const QString &actionName = object != nullptr ? object->objectName() : QString();
383

384
    if (actionName == QLatin1String("copy-action")) {
385
386
        QApplication::clipboard()->setText(url);
        return;
387
388
    }

389
    if ((object == nullptr) || actionName == QLatin1String("open-action")) {
Kurt Hindenburg's avatar
Kurt Hindenburg committed
390
        if (kind == StandardUrl) {
391
            // if the URL path does not include the protocol ( eg. "www.kde.org" ) then
392
            // prepend https:// ( eg. "www.kde.org" --> "https://www.kde.org" )
393
            if (!url.contains(QLatin1String("://"))) {
394
                url.prepend(QLatin1String("https://"));
395
            }
Kurt Hindenburg's avatar
Kurt Hindenburg committed
396
        } else if (kind == Email) {
397
            url.prepend(QLatin1String("mailto:"));
398
        }
399

Kurt Hindenburg's avatar
Kurt Hindenburg committed
400
        new KRun(QUrl(url), QApplication::activeWindow());
401
402
    }
}
403

Kurt Hindenburg's avatar
Kurt Hindenburg committed
404
// Note:  Altering these regular expressions can have a major effect on the performance of the filters
405
406
407
408
// used for finding URLs in the text, especially if they are very general and could match very long
// pieces of text.
// Please be careful when altering them.

409
//regexp matches:
Kurt Hindenburg's avatar
Kurt Hindenburg committed
410
// full url:
411
// protocolname:// or www. followed by anything other than whitespaces, <, >, ' or ", and ends before whitespaces, <, >, ', ", ], !, ), :, comma and dot
412
const QRegularExpression UrlFilter::FullUrlRegExp(QStringLiteral("(www\\.(?!\\.)|[a-z][a-z0-9+.-]*://)[^\\s<>'\"]+[^!,\\.\\s<>'\"\\]\\)\\:]"),
413
                                                  QRegularExpression::OptimizeOnFirstUsageOption);
414
415
// email address:
// [word chars, dots or dashes]@[word chars, dots or dashes].[word chars]
416
const QRegularExpression UrlFilter::EmailAddressRegExp(QStringLiteral("\\b(\\w|\\.|-|\\+)+@(\\w|\\.|-)+\\.\\w+\\b"),
417
                                                       QRegularExpression::OptimizeOnFirstUsageOption);
418
419

// matches full url or email address
Kurt Hindenburg's avatar
Kurt Hindenburg committed
420
421
const QRegularExpression UrlFilter::CompleteUrlRegExp(QLatin1Char('(') + FullUrlRegExp.pattern() + QLatin1Char('|')
                                                      + EmailAddressRegExp.pattern() + QLatin1Char(')'),
422
                                                      QRegularExpression::OptimizeOnFirstUsageOption);
423

424
425
UrlFilter::UrlFilter()
{
Kurt Hindenburg's avatar
Kurt Hindenburg committed
426
    setRegExp(CompleteUrlRegExp);
427
}
Kurt Hindenburg's avatar
Kurt Hindenburg committed
428

429
UrlFilter::HotSpot::~HotSpot() = default;
Kurt Hindenburg's avatar
Kurt Hindenburg committed
430

Tomaz  Canabrava's avatar
Tomaz Canabrava committed
431
QList<QAction *> UrlFilter::HotSpot::actions()
432
{
433
434
    auto openAction = new QAction(this);
    auto copyAction = new QAction(this);
435

Jekyll Wu's avatar
Jekyll Wu committed
436
    const UrlType kind = urlType();
Kurt Hindenburg's avatar
Kurt Hindenburg committed
437
    Q_ASSERT(kind == StandardUrl || kind == Email);
438

Kurt Hindenburg's avatar
Kurt Hindenburg committed
439
    if (kind == StandardUrl) {
440
        openAction->setText(i18n("Open Link"));
441
        openAction->setIcon(QIcon::fromTheme(QStringLiteral("internet-services")));
442
        copyAction->setText(i18n("Copy Link Address"));
443
        copyAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy-url")));
Kurt Hindenburg's avatar
Kurt Hindenburg committed
444
    } else if (kind == Email) {
445
        openAction->setText(i18n("Send Email To..."));
446
        openAction->setIcon(QIcon::fromTheme(QStringLiteral("mail-send")));
447
        copyAction->setText(i18n("Copy Email Address"));
448
        copyAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy-mail")));
449
450
451
452
453
    }

    // object names are set here so that the hotspot performs the
    // correct action when activated() is called with the triggered
    // action passed as a parameter.
454
455
    openAction->setObjectName(QStringLiteral("open-action"));
    copyAction->setObjectName(QStringLiteral("copy-action"));
456

457
458
    QObject::connect(openAction, &QAction::triggered, this, [this, openAction]{ activate(openAction); });
    QObject::connect(copyAction, &QAction::triggered, this, [this, copyAction]{ activate(copyAction); });
459

460
    return {openAction, copyAction};
461
462
}

463
464
465
466
/**
  * File Filter - Construct a filter that works on local file paths using the
  * posix portable filename character set combined with KDE's mimetype filename
  * extension blob patterns.
467
  * https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_267
468
469
  */

470
QSharedPointer<Filter::HotSpot> FileFilter::newHotSpot(int startLine, int startColumn, int endLine,
Kurt Hindenburg's avatar
Kurt Hindenburg committed
471
                                              int endColumn, const QStringList &capturedTexts)
472
{
473
    if (_session.isNull()) {
474
        qCDebug(KonsoleDebug) << "Trying to create new hot spot without session!";
475
476
477
        return nullptr;
    }

478
479
480
481
    QString filename = capturedTexts.first();
    if (filename.startsWith(QLatin1Char('\'')) && filename.endsWith(QLatin1Char('\''))) {
        filename.remove(0, 1);
        filename.chop(1);
Kurt Hindenburg's avatar
Kurt Hindenburg committed
482
    }
483

484
485
486
    // Return nullptr if it's not:
    // <current dir>/filename
    // <current dir>/childDir/filename
Tomaz  Canabrava's avatar
Tomaz Canabrava committed
487
488
489
490
    auto match = std::find_if(std::begin(_currentDirContents), std::end(_currentDirContents),
        [filename](const QString& s) { return filename.startsWith(s); });

    if (match == std::end(_currentDirContents)) {
491
492
493
        return nullptr;
    }

494
    return QSharedPointer<Filter::HotSpot>(new FileFilter::HotSpot(startLine, startColumn, endLine, endColumn, capturedTexts, _dirPath + filename));
495
496
497
498
499
500
}

void FileFilter::process()
{
    const QDir dir(_session->currentWorkingDirectory());
    _dirPath = dir.canonicalPath() + QLatin1Char('/');
501
    _currentDirContents = dir.entryList(QDir::Dirs | QDir::Files);
502
503

    RegExpFilter::process();
504
505
}

Kurt Hindenburg's avatar
Kurt Hindenburg committed
506
FileFilter::HotSpot::HotSpot(int startLine, int startColumn, int endLine, int endColumn,
507
                             const QStringList &capturedTexts, const QString &filePath) :
Kurt Hindenburg's avatar
Kurt Hindenburg committed
508
    RegExpFilter::HotSpot(startLine, startColumn, endLine, endColumn, capturedTexts),
509
    _filePath(filePath)
510
511
512
513
{
    setType(Link);
}

Kurt Hindenburg's avatar
Kurt Hindenburg committed
514
void FileFilter::HotSpot::activate(QObject *)
515
{
516
517
518
    new KRun(QUrl::fromLocalFile(_filePath), QApplication::activeWindow());
}

Kurt Hindenburg's avatar
Kurt Hindenburg committed
519
520
FileFilter::FileFilter(Session *session) :
    _session(session)
521
    , _dirPath(QString())
522
523
    , _currentDirContents(QStringList())
{
524
525
526
527
528
529
530
531
532
533
    static auto re = QRegularExpression(
        /* First part of the regexp means 'strings with spaces and starting with single quotes'
         * Second part means "Strings with double quotes"
         * Last part means "Everything else plus some special chars
         * This is much smaller, and faster, than the previous regexp
         * on the HotSpot creation we verify if this is indeed a file, so there's
         * no problem on testing on random words on the screen.
         */
        QLatin1String(R"('[^']+'|"[^"]+"|[\w.~:]+)"),
        QRegularExpression::DontCaptureOption);
534
    setRegExp(re);
535
536
}

537
FileFilter::HotSpot::~HotSpot() = default;
538

Tomaz  Canabrava's avatar
Tomaz Canabrava committed
539
QList<QAction *> FileFilter::HotSpot::actions()
540
{
541
    auto openAction = new QAction(this);
542
    openAction->setText(i18n("Open File"));
543
544
    QObject::connect(openAction, &QAction::triggered, this, [this ]{ activate(); });
    return {openAction};
545
}