1/*
2 SPDX-FileCopyrightText: 2008 Paul Giannaros <paul@giannaros.org>
3 SPDX-FileCopyrightText: 2009-2018 Dominik Haumann <dhaumann@kde.org>
4 SPDX-FileCopyrightText: 2010 Joseph Wenninger <jowenn@kde.org>
5 SPDX-FileCopyrightText: 2025 Mirian Margiani <mixosaurus+kde@pm.me>
6
7 SPDX-License-Identifier: LGPL-2.0-or-later
8*/
9
10#include "katescript.h"
11
12#include "katepartdebug.h"
13#include "katescriptdocument.h"
14#include "katescripteditor.h"
15#include "katescripthelpers.h"
16#include "katescriptview.h"
17#include "kateview.h"
18
19#include <KLocalizedString>
20#include <iostream>
21
22#include <QFile>
23#include <QFileInfo>
24#include <QJSEngine>
25#include <QQmlEngine>
26
27KateScript::KateScript(const QString &urlOrScript, enum InputType inputType)
28 : m_url(inputType == InputURL ? urlOrScript : QString())
29 , m_inputType(inputType)
30 , m_script(inputType == InputSCRIPT ? urlOrScript : QString())
31{
32}
33
34KateScript::~KateScript()
35{
36 if (m_loadSuccessful) {
37 // remove data...
38 delete m_editor;
39 delete m_document;
40 delete m_view;
41 delete m_engine;
42 }
43}
44
45QString KateScript::backtrace(const QJSValue &error, const QString &header)
46{
47 QString bt;
48 if (!header.isNull()) {
49 bt += header + QLatin1String(":\n");
50 }
51 if (error.isError()) {
52 bt += error.toString() + QLatin1String("\nStrack trace:\n") + error.property(QStringLiteral("stack")).toString();
53 }
54
55 return bt;
56}
57
58void KateScript::displayBacktrace(const QJSValue &error, const QString &header)
59{
60 if (!m_engine) {
61 std::cerr << "KateScript::displayBacktrace: no engine, cannot display error\n";
62 return;
63 }
64 std::cerr << "\033[31m" << qPrintable(backtrace(error, header)) << "\033[0m" << '\n';
65}
66
67void KateScript::clearExceptions()
68{
69 if (!load()) {
70 return;
71 }
72}
73
74QJSValue KateScript::global(const QString &name)
75{
76 // load the script if necessary
77 if (!load()) {
78 return QJSValue::UndefinedValue;
79 }
80 return m_engine->globalObject().property(name);
81}
82
83QJSValue KateScript::function(const QString &name)
84{
85 QJSValue value = global(name);
86 if (!value.isCallable()) {
87 return QJSValue::UndefinedValue;
88 }
89 return value;
90}
91
92bool KateScript::load()
93{
94 if (m_loaded) {
95 return m_loadSuccessful;
96 }
97
98 m_loaded = true;
99 m_loadSuccessful = false; // here set to false, and at end of function to true
100
101 // read the script file into memory
102 QString source;
103 if (m_inputType == InputURL) {
104 if (!Kate::Script::readFile(sourceUrl: m_url, sourceCode&: source)) {
105 return false;
106 }
107 } else {
108 source = m_script;
109 }
110
111 // create script engine, register meta types
112 m_engine = new QJSEngine();
113
114 // export read & require function and add the require guard object
115 auto scriptHelper = new Kate::ScriptHelper(m_engine);
116 QJSValue functions = m_engine->newQObject(object: scriptHelper);
117 m_engine->globalObject().setProperty(QStringLiteral("functions"), value: functions);
118 m_engine->globalObject().setProperty(QStringLiteral("read"), value: functions.property(QStringLiteral("read")));
119 m_engine->globalObject().setProperty(QStringLiteral("require"), value: functions.property(QStringLiteral("require")));
120 m_engine->globalObject().setProperty(QStringLiteral("require_guard"), value: m_engine->newObject());
121
122 // View and Document expose JS Range objects in the API, which will fail to work
123 // if Range is not included. range.js includes cursor.js
124 scriptHelper->require(QStringLiteral("range.js"));
125
126 // export debug function
127 m_engine->globalObject().setProperty(QStringLiteral("debug"), value: functions.property(QStringLiteral("debug")));
128
129 // export translation functions
130 m_engine->globalObject().setProperty(QStringLiteral("i18n"), value: functions.property(QStringLiteral("_i18n")));
131 m_engine->globalObject().setProperty(QStringLiteral("i18nc"), value: functions.property(QStringLiteral("_i18nc")));
132 m_engine->globalObject().setProperty(QStringLiteral("i18np"), value: functions.property(QStringLiteral("_i18np")));
133 m_engine->globalObject().setProperty(QStringLiteral("i18ncp"), value: functions.property(QStringLiteral("_i18ncp")));
134
135 // register default styles as ds* global properties
136 m_engine->globalObject().setProperty(QStringLiteral("dsNormal"), value: KSyntaxHighlighting::Theme::TextStyle::Normal);
137 m_engine->globalObject().setProperty(QStringLiteral("dsKeyword"), value: KSyntaxHighlighting::Theme::TextStyle::Keyword);
138 m_engine->globalObject().setProperty(QStringLiteral("dsFunction"), value: KSyntaxHighlighting::Theme::TextStyle::Function);
139 m_engine->globalObject().setProperty(QStringLiteral("dsVariable"), value: KSyntaxHighlighting::Theme::TextStyle::Variable);
140 m_engine->globalObject().setProperty(QStringLiteral("dsControlFlow"), value: KSyntaxHighlighting::Theme::TextStyle::ControlFlow);
141 m_engine->globalObject().setProperty(QStringLiteral("dsOperator"), value: KSyntaxHighlighting::Theme::TextStyle::Operator);
142 m_engine->globalObject().setProperty(QStringLiteral("dsBuiltIn"), value: KSyntaxHighlighting::Theme::TextStyle::BuiltIn);
143 m_engine->globalObject().setProperty(QStringLiteral("dsExtension"), value: KSyntaxHighlighting::Theme::TextStyle::Extension);
144 m_engine->globalObject().setProperty(QStringLiteral("dsPreprocessor"), value: KSyntaxHighlighting::Theme::TextStyle::Preprocessor);
145 m_engine->globalObject().setProperty(QStringLiteral("dsAttribute"), value: KSyntaxHighlighting::Theme::TextStyle::Attribute);
146 m_engine->globalObject().setProperty(QStringLiteral("dsChar"), value: KSyntaxHighlighting::Theme::TextStyle::Char);
147 m_engine->globalObject().setProperty(QStringLiteral("dsSpecialChar"), value: KSyntaxHighlighting::Theme::TextStyle::SpecialChar);
148 m_engine->globalObject().setProperty(QStringLiteral("dsString"), value: KSyntaxHighlighting::Theme::TextStyle::String);
149 m_engine->globalObject().setProperty(QStringLiteral("dsVerbatimString"), value: KSyntaxHighlighting::Theme::TextStyle::VerbatimString);
150 m_engine->globalObject().setProperty(QStringLiteral("dsSpecialString"), value: KSyntaxHighlighting::Theme::TextStyle::SpecialString);
151 m_engine->globalObject().setProperty(QStringLiteral("dsImport"), value: KSyntaxHighlighting::Theme::TextStyle::Import);
152 m_engine->globalObject().setProperty(QStringLiteral("dsDataType"), value: KSyntaxHighlighting::Theme::TextStyle::DataType);
153 m_engine->globalObject().setProperty(QStringLiteral("dsDecVal"), value: KSyntaxHighlighting::Theme::TextStyle::DecVal);
154 m_engine->globalObject().setProperty(QStringLiteral("dsBaseN"), value: KSyntaxHighlighting::Theme::TextStyle::BaseN);
155 m_engine->globalObject().setProperty(QStringLiteral("dsFloat"), value: KSyntaxHighlighting::Theme::TextStyle::Float);
156 m_engine->globalObject().setProperty(QStringLiteral("dsConstant"), value: KSyntaxHighlighting::Theme::TextStyle::Constant);
157 m_engine->globalObject().setProperty(QStringLiteral("dsComment"), value: KSyntaxHighlighting::Theme::TextStyle::Comment);
158 m_engine->globalObject().setProperty(QStringLiteral("dsDocumentation"), value: KSyntaxHighlighting::Theme::TextStyle::Documentation);
159 m_engine->globalObject().setProperty(QStringLiteral("dsAnnotation"), value: KSyntaxHighlighting::Theme::TextStyle::Annotation);
160 m_engine->globalObject().setProperty(QStringLiteral("dsCommentVar"), value: KSyntaxHighlighting::Theme::TextStyle::CommentVar);
161 m_engine->globalObject().setProperty(QStringLiteral("dsRegionMarker"), value: KSyntaxHighlighting::Theme::TextStyle::RegionMarker);
162 m_engine->globalObject().setProperty(QStringLiteral("dsInformation"), value: KSyntaxHighlighting::Theme::TextStyle::Information);
163 m_engine->globalObject().setProperty(QStringLiteral("dsWarning"), value: KSyntaxHighlighting::Theme::TextStyle::Warning);
164 m_engine->globalObject().setProperty(QStringLiteral("dsAlert"), value: KSyntaxHighlighting::Theme::TextStyle::Alert);
165 m_engine->globalObject().setProperty(QStringLiteral("dsOthers"), value: KSyntaxHighlighting::Theme::TextStyle::Others);
166 m_engine->globalObject().setProperty(QStringLiteral("dsError"), value: KSyntaxHighlighting::Theme::TextStyle::Error);
167
168 // register scripts itself
169 QJSValue result = m_engine->evaluate(program: source, fileName: m_url);
170 if (hasException(object: result, file: m_url)) {
171 return false;
172 }
173
174 // AFTER SCRIPT: set the view/document objects as necessary
175 m_engine->globalObject().setProperty(QStringLiteral("editor"), value: m_engine->newQObject(object: m_editor = new KateScriptEditor()));
176 m_engine->globalObject().setProperty(QStringLiteral("document"), value: m_engine->newQObject(object: m_document = new KateScriptDocument(m_engine)));
177 m_engine->globalObject().setProperty(QStringLiteral("view"), value: m_engine->newQObject(object: m_view = new KateScriptView(m_engine)));
178
179 // yip yip!
180 m_loadSuccessful = true;
181
182 return true;
183}
184
185QJSValue KateScript::evaluate(const QString &program, const FieldMap &env)
186{
187 if (!load()) {
188 qCWarning(LOG_KTE) << "load of script failed:" << program;
189 return QJSValue();
190 }
191
192 // JS reserved words that are not allowed as variable names
193 // source: https://web.archive.org/web/20250415020612/https://www.w3schools.com/js/js_reserved.asp
194 // exceptions: Java Reserved Words, Other Reserved Words, HTML Event Handlers,
195 // plus "length", "name", "prototype", "hasOwnProperty", "package"
196 static const auto invalidRe =
197 QRegularExpression{QStringLiteral("^(Array|Date|Infinity|Math|NaN|Number|Object|String|abstract|arguments|await|"
198 "boolean|break|byte|case|catch|char|class|const|continue|debugger|default|delete|"
199 "do|double|else|enum|eval|export|extends|false|final|finally|float|for|function|goto|"
200 "if|implements|import|in|instanceof|int|interface|isFinite|isNaN|isPrototypeOf|let|long|"
201 "native|new|null|private|protected|public|return|short|static|super|switch|synchronized|"
202 "this|throw|throws|toString|transient|true|try|typeof|undefined|valueOf|"
203 "var|void|volatile|while|with|yield)$")};
204 static const auto validRe = QRegularExpression{QStringLiteral("^[a-zA-Z0-9_]+$")};
205
206 auto filteredKeys = QStringList{};
207 auto paramKeys = QStringList{};
208 auto paramValues = QJSValueList{};
209 paramKeys.reserve(asize: env.size());
210 paramValues.reserve(asize: env.size());
211
212 paramKeys << QStringLiteral("__program__");
213 paramValues << program;
214
215 auto fields = m_engine->newObject();
216
217 for (const auto &k : env.keys()) {
218 fields.setProperty(name: k, value: env[k]);
219
220 // Skip fields that would overwrite global properties
221 if (m_engine->globalObject().hasProperty(name: k) || k == QStringLiteral("__program__") || k == QStringLiteral("fields")) {
222 filteredKeys << k;
223 continue;
224 }
225
226 auto matchValid = validRe.matchView(subjectView: k);
227
228 if (matchValid.hasMatch()) {
229 auto matchInvalid = invalidRe.match(subject: k);
230
231 if (matchInvalid.hasMatch()) {
232 filteredKeys << k;
233 continue;
234 }
235 } else {
236 filteredKeys << k;
237 continue;
238 }
239
240 paramKeys << k;
241 paramValues << env[k];
242 }
243
244 // Export the 'fields' map so that any function has access to
245 // the current fields, even if the names are invalid JS identifiers
246 m_engine->globalObject().setProperty(QStringLiteral("fields"), value: fields);
247
248 // Wrap the arguments in a function to avoid polluting the global object
249 auto programWithContext = QStringLiteral("(function(%1){ return eval(__program__); })").arg(a: paramKeys.join(sep: QLatin1Char(',')));
250
251 auto programFunction = m_engine->evaluate(program: programWithContext);
252 QJSValue result;
253
254 if (programFunction.isCallable()) {
255 result = programFunction.call(args: paramValues);
256 } else {
257 qCWarning(LOG_KTE) << "Error evaluating script: " << programWithContext;
258 result = QStringLiteral("Bug: unable to evaluate script");
259 }
260
261 if (result.isError()) {
262 if (result.errorType() == QJSValue::ReferenceError) {
263 auto var = result.property(QStringLiteral("message")).toString().split(sep: QLatin1Char(' '))[0];
264
265 if (filteredKeys.contains(str: var)) {
266 result = QStringLiteral("SyntaxError: access %1 through the fields map").arg(a: var);
267 }
268 }
269
270 qCWarning(LOG_KTE) << "Error evaluating script: " << result.toString();
271 }
272
273 // Reset the 'fields' map to clean up the global object
274 m_engine->globalObject().deleteProperty(QStringLiteral("fields"));
275
276 return result;
277}
278
279bool KateScript::hasException(const QJSValue &object, const QString &file)
280{
281 if (object.isError()) {
282 m_errorMessage = i18n("Error loading script %1: %2", file, object.toString());
283 displayBacktrace(error: object, header: m_errorMessage);
284 delete m_engine;
285 m_engine = nullptr;
286 m_loadSuccessful = false;
287 return true;
288 }
289 return false;
290}
291
292bool KateScript::setView(KTextEditor::ViewPrivate *view)
293{
294 if (!load()) {
295 return false;
296 }
297 // setup the stuff
298 m_document->setDocument(view->doc());
299 m_view->setView(view);
300 return true;
301}
302
303void KateScript::setGeneralHeader(const KateScriptHeader &generalHeader)
304{
305 m_generalHeader = generalHeader;
306}
307
308KateScriptHeader &KateScript::generalHeader()
309{
310 return m_generalHeader;
311}
312

source code of ktexteditor/src/script/katescript.cpp