1 /*
2  * JSON Tiled Plugin
3  * Copyright 2011, Porfírio José Pereira Ribeiro <porfirioribeiro@gmail.com>
4  * Copyright 2011, Thorbjørn Lindeijer <thorbjorn@lindeijer.nl>
5  *
6  * This file is part of Tiled.
7  *
8  * This program is free software; you can redistribute it and/or modify it
9  * under the terms of the GNU General Public License as published by the Free
10  * Software Foundation; either version 2 of the License, or (at your option)
11  * any later version.
12  *
13  * This program is distributed in the hope that it will be useful, but WITHOUT
14  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
15  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
16  * more details.
17  *
18  * You should have received a copy of the GNU General Public License along with
19  * this program. If not, see <http://www.gnu.org/licenses/>.
20  */
21 
22 #include "jsonplugin.h"
23 
24 #include "maptovariantconverter.h"
25 #include "varianttomapconverter.h"
26 #include "savefile.h"
27 
28 #include "qjsonparser/json.h"
29 
30 #include <QCoreApplication>
31 #include <QFile>
32 #include <QFileInfo>
33 #include <QJsonDocument>
34 #include <QJsonObject>
35 #include <QTextStream>
36 
37 namespace Json {
38 
initialize()39 void JsonPlugin::initialize()
40 {
41     addObject(new JsonMapFormat(JsonMapFormat::Json, this));
42     addObject(new JsonMapFormat(JsonMapFormat::JavaScript, this));
43     addObject(new JsonTilesetFormat(this));
44     addObject(new JsonObjectTemplateFormat(this));
45 }
46 
47 
JsonMapFormat(SubFormat subFormat,QObject * parent)48 JsonMapFormat::JsonMapFormat(SubFormat subFormat, QObject *parent)
49     : Tiled::MapFormat(parent)
50     , mSubFormat(subFormat)
51 {}
52 
read(const QString & fileName)53 std::unique_ptr<Tiled::Map> JsonMapFormat::read(const QString &fileName)
54 {
55     QFile file(fileName);
56     if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
57         mError = QCoreApplication::translate("File Errors", "Could not open file for reading.");
58         return nullptr;
59     }
60 
61     QByteArray contents = file.readAll();
62     if (mSubFormat == JavaScript && contents.size() > 0 && contents[0] != '{') {
63         // Scan past JSONP prefix; look for an open curly at the start of the line
64         int i = contents.indexOf("\n{");
65         if (i > 0) {
66             contents.remove(0, i);
67             contents = contents.trimmed(); // potential trailing whitespace
68             if (contents.endsWith(';')) contents.chop(1);
69             if (contents.endsWith(')')) contents.chop(1);
70         }
71     }
72 
73     QJsonParseError error;
74     const QJsonDocument document = QJsonDocument::fromJson(contents, &error);
75 
76     if (error.error != QJsonParseError::NoError) {
77         mError = tr("Error parsing file: %1").arg(error.errorString());
78         return nullptr;
79     }
80 
81     Tiled::VariantToMapConverter converter;
82     auto map = converter.toMap(document.toVariant(), QFileInfo(fileName).dir());
83 
84     if (!map)
85         mError = converter.errorString();
86 
87     return map;
88 }
89 
write(const Tiled::Map * map,const QString & fileName,Options options)90 bool JsonMapFormat::write(const Tiled::Map *map,
91                           const QString &fileName,
92                           Options options)
93 {
94     Tiled::SaveFile file(fileName);
95 
96     if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
97         mError = QCoreApplication::translate("File Errors", "Could not open file for writing.");
98         return false;
99     }
100 
101     Tiled::MapToVariantConverter converter{1};
102     QVariant variant = converter.toVariant(*map, QFileInfo(fileName).dir());
103 
104     JsonWriter writer;
105     writer.setAutoFormatting(!options.testFlag(WriteMinimized));
106 
107     if (!writer.stringify(variant)) {
108         // This can only happen due to coding error
109         mError = writer.errorString();
110         return false;
111     }
112 
113     QTextStream out(file.device());
114     if (mSubFormat == JavaScript) {
115         // Trim and escape name
116         JsonWriter nameWriter;
117         QString baseName = QFileInfo(fileName).baseName();
118         nameWriter.stringify(baseName);
119         out << "(function(name,data){\n if(typeof onTileMapLoaded === 'undefined') {\n";
120         out << "  if(typeof TileMaps === 'undefined') TileMaps = {};\n";
121         out << "  TileMaps[name] = data;\n";
122         out << " } else {\n";
123         out << "  onTileMapLoaded(name,data);\n";
124         out << " }\n";
125         out << " if(typeof module === 'object' && module && module.exports) {\n";
126         out << "  module.exports = data;\n";
127         out << " }})(" << nameWriter.result() << ",\n";
128     }
129     out << writer.result();
130     if (mSubFormat == JavaScript) {
131         out << ");";
132     }
133 
134     if (file.error() != QFileDevice::NoError) {
135         mError = tr("Error while writing file:\n%1").arg(file.errorString());
136         return false;
137     }
138 
139     if (!file.commit()) {
140         mError = file.errorString();
141         return false;
142     }
143 
144     return true;
145 }
146 
nameFilter() const147 QString JsonMapFormat::nameFilter() const
148 {
149     if (mSubFormat == Json)
150         return tr("JSON map files [Tiled 1.1] (*.json)");
151     else
152         return tr("JavaScript map files [Tiled 1.1] (*.js)");
153 }
154 
shortName() const155 QString JsonMapFormat::shortName() const
156 {
157     if (mSubFormat == Json)
158         return QStringLiteral("json1");
159     else
160         return QStringLiteral("js1");
161 }
162 
supportsFile(const QString & fileName) const163 bool JsonMapFormat::supportsFile(const QString &fileName) const
164 {
165     if (mSubFormat == Json) {
166         if (!fileName.endsWith(QLatin1String(".json"), Qt::CaseInsensitive))
167             return false;
168     } else {
169         if (!fileName.endsWith(QLatin1String(".js"), Qt::CaseInsensitive))
170             return false;
171     }
172 
173     QFile file(fileName);
174     if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
175         QByteArray contents = file.readAll();
176 
177         if (mSubFormat == JavaScript && contents.size() > 0 && contents[0] != '{') {
178             // Scan past JSONP prefix; look for an open curly at the start of the line
179             int i = contents.indexOf("\n{");
180             if (i > 0) {
181                 contents.remove(0, i);
182                 contents = contents.trimmed(); // potential trailing whitespace
183                 if (contents.endsWith(';')) contents.chop(1);
184                 if (contents.endsWith(')')) contents.chop(1);
185             }
186         }
187 
188         const QJsonObject object = QJsonDocument::fromJson(contents).object();
189 
190         // This is a good indication, but not present in older map files
191         if (object.value(QLatin1String("type")).toString() == QLatin1String("map"))
192             return true;
193 
194         // Guess based on expected property
195         if (object.contains(QLatin1String("orientation")))
196             return true;
197     }
198 
199     return false;
200 }
201 
errorString() const202 QString JsonMapFormat::errorString() const
203 {
204     return mError;
205 }
206 
207 
JsonTilesetFormat(QObject * parent)208 JsonTilesetFormat::JsonTilesetFormat(QObject *parent)
209     : Tiled::TilesetFormat(parent)
210 {
211 }
212 
read(const QString & fileName)213 Tiled::SharedTileset JsonTilesetFormat::read(const QString &fileName)
214 {
215     QFile file(fileName);
216 
217     if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
218         mError = QCoreApplication::translate("File Errors", "Could not open file for reading.");
219         return Tiled::SharedTileset();
220     }
221 
222     QJsonParseError error;
223     const QJsonDocument document = QJsonDocument::fromJson(file.readAll(), &error);
224 
225     if (error.error != QJsonParseError::NoError) {
226         mError = tr("Error parsing file: %1").arg(error.errorString());
227         return Tiled::SharedTileset();
228     }
229 
230     Tiled::VariantToMapConverter converter;
231     Tiled::SharedTileset tileset = converter.toTileset(document.toVariant(),
232                                                        QFileInfo(fileName).dir());
233 
234     if (!tileset)
235         mError = converter.errorString();
236 
237     return tileset;
238 }
239 
supportsFile(const QString & fileName) const240 bool JsonTilesetFormat::supportsFile(const QString &fileName) const
241 {
242     if (fileName.endsWith(QLatin1String(".json"), Qt::CaseInsensitive)) {
243         QFile file(fileName);
244 
245         if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
246             const QJsonObject object = QJsonDocument::fromJson(file.readAll()).object();
247 
248             // This is a good indication, but not present in older external tilesets
249             if (object.value(QLatin1String("type")).toString() == QLatin1String("tileset"))
250                 return true;
251 
252             // Guess based on some expected properties
253             if (object.contains(QLatin1String("name")) &&
254                 object.contains(QLatin1String("tilewidth")) &&
255                 object.contains(QLatin1String("tileheight")))
256                 return true;
257         }
258     }
259 
260     return false;
261 }
262 
write(const Tiled::Tileset & tileset,const QString & fileName,Options options)263 bool JsonTilesetFormat::write(const Tiled::Tileset &tileset,
264                               const QString &fileName,
265                               Options options)
266 {
267     Tiled::SaveFile file(fileName);
268 
269     if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
270         mError = QCoreApplication::translate("File Errors", "Could not open file for writing.");
271         return false;
272     }
273 
274     Tiled::MapToVariantConverter converter{1};
275     QVariant variant = converter.toVariant(tileset, QFileInfo(fileName).dir());
276 
277     JsonWriter writer;
278     writer.setAutoFormatting(!options.testFlag(WriteMinimized));
279 
280     if (!writer.stringify(variant)) {
281         // This can only happen due to coding error
282         mError = writer.errorString();
283         return false;
284     }
285 
286     QTextStream out(file.device());
287     out << writer.result();
288 
289     if (file.error() != QFileDevice::NoError) {
290         mError = tr("Error while writing file:\n%1").arg(file.errorString());
291         return false;
292     }
293 
294     if (!file.commit()) {
295         mError = file.errorString();
296         return false;
297     }
298 
299     return true;
300 }
301 
nameFilter() const302 QString JsonTilesetFormat::nameFilter() const
303 {
304     return tr("JSON tileset files [Tiled 1.1] (*.json)");
305 }
306 
shortName() const307 QString JsonTilesetFormat::shortName() const
308 {
309     return QStringLiteral("json1");
310 }
311 
errorString() const312 QString JsonTilesetFormat::errorString() const
313 {
314     return mError;
315 }
316 
JsonObjectTemplateFormat(QObject * parent)317 JsonObjectTemplateFormat::JsonObjectTemplateFormat(QObject *parent)
318     : Tiled::ObjectTemplateFormat(parent)
319 {
320 }
321 
read(const QString & fileName)322 std::unique_ptr<Tiled::ObjectTemplate> JsonObjectTemplateFormat::read(const QString &fileName)
323 {
324     QFile file(fileName);
325 
326     if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
327         mError = QCoreApplication::translate("File Errors", "Could not open file for reading.");
328         return nullptr;
329     }
330 
331     QJsonParseError error;
332     const QJsonDocument document = QJsonDocument::fromJson(file.readAll(), &error);
333 
334     if (error.error != QJsonParseError::NoError) {
335         mError = tr("Error parsing file: %1").arg(error.errorString());
336         return nullptr;
337     }
338 
339     Tiled::VariantToMapConverter converter;
340     auto objectTemplate = converter.toObjectTemplate(document.toVariant(),
341                                                      QFileInfo(fileName).dir());
342 
343     if (!objectTemplate)
344         mError = converter.errorString();
345     else
346         objectTemplate->setFileName(fileName);
347 
348     return objectTemplate;
349 }
350 
supportsFile(const QString & fileName) const351 bool JsonObjectTemplateFormat::supportsFile(const QString &fileName) const
352 {
353     if (fileName.endsWith(QLatin1String(".json"), Qt::CaseInsensitive)) {
354         QFile file(fileName);
355 
356         if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
357             const QJsonObject object = QJsonDocument::fromJson(file.readAll()).object();
358 
359             if (object.value(QLatin1String("type")).toString() == QLatin1String("template"))
360                 return true;
361         }
362     }
363 
364     return false;
365 }
366 
write(const Tiled::ObjectTemplate * objectTemplate,const QString & fileName)367 bool JsonObjectTemplateFormat::write(const Tiled::ObjectTemplate *objectTemplate, const QString &fileName)
368 {
369     Tiled::SaveFile file(fileName);
370 
371     if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
372         mError = QCoreApplication::translate("File Errors", "Could not open file for writing.");
373         return false;
374     }
375 
376     Tiled::MapToVariantConverter converter{1};
377     QVariant variant = converter.toVariant(*objectTemplate, QFileInfo(fileName).dir());
378 
379     JsonWriter writer;
380     writer.setAutoFormatting(true);
381 
382     if (!writer.stringify(variant)) {
383         // This can only happen due to coding error
384         mError = writer.errorString();
385         return false;
386     }
387 
388     QTextStream out(file.device());
389     out << writer.result();
390 
391     if (file.error() != QFileDevice::NoError) {
392         mError = tr("Error while writing file:\n%1").arg(file.errorString());
393         return false;
394     }
395 
396     if (!file.commit()) {
397         mError = file.errorString();
398         return false;
399     }
400 
401     return true;
402 }
403 
nameFilter() const404 QString JsonObjectTemplateFormat::nameFilter() const
405 {
406     return tr("JSON template files [Tiled 1.1] (*.json)");
407 }
408 
shortName() const409 QString JsonObjectTemplateFormat::shortName() const
410 {
411     return QStringLiteral("json1");
412 }
413 
errorString() const414 QString JsonObjectTemplateFormat::errorString() const
415 {
416     return mError;
417 }
418 
419 } // namespace Json
420