1 /*
2  * scriptedfileformat.cpp
3  * Copyright 2019, Thorbjørn Lindeijer <bjorn@lindeijer.nl>
4  *
5  * This file is part of Tiled.
6  *
7  * This program is free software; you can redistribute it and/or modify it
8  * under the terms of the GNU General Public License as published by the Free
9  * Software Foundation; either version 2 of the License, or (at your option)
10  * any later version.
11  *
12  * This program is distributed in the hope that it will be useful, but WITHOUT
13  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
14  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
15  * more details.
16  *
17  * You should have received a copy of the GNU General Public License along with
18  * this program. If not, see <http://www.gnu.org/licenses/>.
19  */
20 
21 #include "scriptedfileformat.h"
22 
23 #include "editablemap.h"
24 #include "editabletileset.h"
25 #include "savefile.h"
26 #include "scriptmanager.h"
27 
28 #include <QCoreApplication>
29 #include <QFile>
30 #include <QJSEngine>
31 #include <QJSValueIterator>
32 #include <QTextStream>
33 
34 namespace Tiled {
35 
ScriptedFileFormat(const QJSValue & object)36 ScriptedFileFormat::ScriptedFileFormat(const QJSValue &object)
37     : mObject(object)
38 {
39 }
40 
capabilities() const41 FileFormat::Capabilities ScriptedFileFormat::capabilities() const
42 {
43     FileFormat::Capabilities capabilities;
44 
45     if (mObject.property(QStringLiteral("read")).isCallable())
46         capabilities |= FileFormat::Read;
47 
48     if (mObject.property(QStringLiteral("write")).isCallable())
49         capabilities |= FileFormat::Write;
50 
51     return capabilities;
52 }
53 
nameFilter() const54 QString ScriptedFileFormat::nameFilter() const
55 {
56     QString name = mObject.property(QStringLiteral("name")).toString();
57     QString extension = mObject.property(QStringLiteral("extension")).toString();
58 
59     return QStringLiteral("%1 (*.%2)").arg(name, extension);
60 }
61 
supportsFile(const QString & fileName) const62 bool ScriptedFileFormat::supportsFile(const QString &fileName) const
63 {
64     QString extension = mObject.property(QStringLiteral("extension")).toString();
65     extension.prepend(QLatin1Char('.'));
66 
67     return fileName.endsWith(extension);
68 }
69 
read(const QString & fileName)70 QJSValue ScriptedFileFormat::read(const QString &fileName)
71 {
72     QJSValueList arguments;
73     arguments.append(fileName);
74 
75     return mObject.property(QStringLiteral("read")).call(arguments);
76 }
77 
write(EditableAsset * asset,const QString & fileName,FileFormat::Options options,QString & error)78 bool ScriptedFileFormat::write(EditableAsset *asset,
79                                const QString &fileName,
80                                FileFormat::Options options,
81                                QString &error)
82 {
83     error.clear();
84 
85     QJSValueList arguments;
86     arguments.append(ScriptManager::instance().engine()->newQObject(asset));
87     arguments.append(fileName);
88     arguments.append(static_cast<FileFormat::Options::Int>(options));
89 
90     QJSValue resultValue = mObject.property(QStringLiteral("write")).call(arguments);
91     if (ScriptManager::instance().checkError(resultValue)) {
92         error = resultValue.toString();
93         return false;
94     }
95 
96     if (resultValue.isString()) {
97         error = resultValue.toString();
98         return error.isEmpty();
99     }
100 
101     if (!resultValue.isUndefined())
102         ScriptManager::instance().throwError(QCoreApplication::translate("Script Errors", "Invalid return value for 'write' (string or undefined expected)"));
103 
104     return true;
105 }
106 
outputFiles(EditableAsset * asset,const QString & fileName) const107 QStringList ScriptedFileFormat::outputFiles(EditableAsset *asset, const QString &fileName) const
108 {
109     QJSValue outputFiles = mObject.property(QStringLiteral("outputFiles"));
110     if (!outputFiles.isCallable())
111         return QStringList(fileName);
112 
113     QJSValueList arguments;
114     arguments.append(ScriptManager::instance().engine()->newQObject(asset));
115     arguments.append(fileName);
116 
117     QJSValue resultValue = outputFiles.call(arguments);
118 
119     if (resultValue.isString())
120         return QStringList(resultValue.toString());
121 
122     if (resultValue.isArray()) {
123         QStringList result;
124         QJSValueIterator iterator(resultValue);
125         while (iterator.next())
126             result.append(iterator.value().toString());
127         return result;
128     }
129 
130     ScriptManager::instance().throwError(QCoreApplication::translate("Script Errors", "Invalid return value for 'outputFiles' (string or array expected)"));
131     return QStringList(fileName);
132 }
133 
validateFileFormatObject(const QJSValue & value)134 bool ScriptedFileFormat::validateFileFormatObject(const QJSValue &value)
135 {
136     const QJSValue nameProperty = value.property(QStringLiteral("name"));
137     const QJSValue extensionProperty = value.property(QStringLiteral("extension"));
138     const QJSValue writeProperty = value.property(QStringLiteral("write"));
139     const QJSValue readProperty = value.property(QStringLiteral("read"));
140 
141     if (!nameProperty.isString()) {
142         ScriptManager::instance().throwError(QCoreApplication::translate("Script Errors", "Invalid file format object (requires string 'name' property)"));
143         return false;
144     }
145 
146     if (!extensionProperty.isString()) {
147         ScriptManager::instance().throwError(QCoreApplication::translate("Script Errors", "Invalid file format object (requires string 'extension' property)"));
148         return false;
149     }
150 
151     if (!writeProperty.isCallable() && !readProperty.isCallable()) {
152         ScriptManager::instance().throwError(QCoreApplication::translate("Script Errors", "Invalid file format object (requires a 'write' and/or 'read' function property)"));
153         return false;
154     }
155 
156     return true;
157 }
158 
159 
ScriptedMapFormat(const QString & shortName,const QJSValue & object,QObject * parent)160 ScriptedMapFormat::ScriptedMapFormat(const QString &shortName,
161                                      const QJSValue &object,
162                                      QObject *parent)
163     : MapFormat(parent)
164     , mShortName(shortName)
165     , mFormat(object)
166 {
167     PluginManager::addObject(this);
168 }
169 
~ScriptedMapFormat()170 ScriptedMapFormat::~ScriptedMapFormat()
171 {
172     PluginManager::removeObject(this);
173 }
174 
outputFiles(const Map * map,const QString & fileName) const175 QStringList ScriptedMapFormat::outputFiles(const Map *map, const QString &fileName) const
176 {
177     EditableMap editable(map);
178     return mFormat.outputFiles(&editable, fileName);
179 }
180 
read(const QString & fileName)181 std::unique_ptr<Map> ScriptedMapFormat::read(const QString &fileName)
182 {
183     mError.clear();
184 
185     QJSValue resultValue = mFormat.read(fileName);
186 
187     if (ScriptManager::instance().checkError(resultValue)) {
188         mError = resultValue.toString();
189         return nullptr;
190     }
191 
192     EditableMap *editableMap = qobject_cast<EditableMap*>(resultValue.toQObject());
193     if (editableMap) {
194         // We need to clone the map here, because the returned map will be
195         // wrapped in a MapDocument, which the EditableMap instance will be
196         // unaware of. Further changes to the map through this editable would
197         // otherwise mess up the undo system.
198         return std::unique_ptr<Map>(editableMap->map()->clone());
199     }
200 
201     return nullptr;
202 }
203 
write(const Map * map,const QString & fileName,Options options)204 bool ScriptedMapFormat::write(const Map *map, const QString &fileName, Options options)
205 {
206     EditableMap editable(map);
207     return mFormat.write(&editable, fileName, options, mError);
208 }
209 
210 
ScriptedTilesetFormat(const QString & shortName,const QJSValue & object,QObject * parent)211 ScriptedTilesetFormat::ScriptedTilesetFormat(const QString &shortName,
212                                              const QJSValue &object,
213                                              QObject *parent)
214     : TilesetFormat(parent)
215     , mShortName(shortName)
216     , mFormat(object)
217 {
218     PluginManager::addObject(this);
219 }
220 
~ScriptedTilesetFormat()221 ScriptedTilesetFormat::~ScriptedTilesetFormat()
222 {
223     PluginManager::removeObject(this);
224 }
225 
read(const QString & fileName)226 SharedTileset ScriptedTilesetFormat::read(const QString &fileName)
227 {
228     mError.clear();
229 
230     QJSValue resultValue = mFormat.read(fileName);
231 
232     if (ScriptManager::instance().checkError(resultValue)) {
233         mError = resultValue.toString();
234         return SharedTileset();
235     }
236 
237     EditableTileset *editableTileset = qobject_cast<EditableTileset*>(resultValue.toQObject());
238     if (editableTileset) {
239         // We need to clone the tileset here, because the returned tileset will
240         // be wrapped in a TilesetDocument, which the EditableTileset instance
241         // will be unaware of. Further changes to the tileset through this
242         // editable would otherwise mess up the undo system.
243         return editableTileset->tileset()->clone();
244     }
245 
246     return SharedTileset();
247 }
248 
write(const Tileset & tileset,const QString & fileName,FileFormat::Options options)249 bool ScriptedTilesetFormat::write(const Tileset &tileset, const QString &fileName, FileFormat::Options options)
250 {
251     EditableTileset editable(&tileset);
252     return mFormat.write(&editable, fileName, options, mError);
253 }
254 
255 } // namespace Tiled
256 
257 #include "moc_scriptedfileformat.cpp"
258