mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-14 01:04:13 +02:00
Open and save projects as .qetz (zipped, opt-in)
Third stage of the zipped project file (#1440). A project is saved as a .qetz when its file name ends in .qetz -- Save As offers it as a second file type, and --resave, --set-titleblock, the script API's save() and the live-mode snapshot follow the name too. Saving a .qet project still writes a .qet; nothing changes unless the user picks .qetz. Opening recognises a .qetz by its content (a zip), not its name, and reads it as bytes: text mode would alter it on Windows. The project document is joined back and read by the same code as a .qet. Crash backups stay plain XML. Drag and drop, the open dialog and the numbering import accept .qetz; the database export names its file from the project's name without assuming ".qet". Parts are now written without added indentation (one line each): the symbol definitions are kept as read, whitespace included, and parts that gained indentation made the next save differ (five forum projects). Tests: tst_resaveunchanged::containerSavesTheSameProject (four examples saved as .qetz, then as .qet, equal the straight .qet; pictures checked to fail without the fix), tst_qetcontainer::noWhitespaceAdded. Outside ctest: 341 of 341 projects QET can open (examples, bugtracker, GitHub, forum) give the same .qet through a .qetz; 181.6 MB of .qet became 36.9 MB of .qetz. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Wga9B92BNurDEtxxtCDxf
This commit is contained in:
+24
-7
@@ -870,6 +870,15 @@ int exportLinks(QETProject &project, const QString &output)
|
||||
int resaveProject(QETProject &project, const QString &output)
|
||||
{
|
||||
const QDomDocument doc = project.toXml();
|
||||
if (QETProject::isContainerPath(output)) {
|
||||
QString error;
|
||||
if (!QETProject::writeContainer(doc, output, &error)) {
|
||||
err << "Cannot write '" << output << "': " << error << "\n";
|
||||
return 1;
|
||||
}
|
||||
out << "Re-saved project -> " << output << "\n";
|
||||
return 0;
|
||||
}
|
||||
QFile file(output);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
err << "Cannot open '" << output << "' for writing.\n";
|
||||
@@ -955,14 +964,22 @@ int setTitleBlock(QETProject &project, const QString &output,
|
||||
}
|
||||
|
||||
const QDomDocument doc = project.toXml();
|
||||
QFile file(output);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
err << "Cannot open '" << output << "' for writing.\n";
|
||||
return 1;
|
||||
if (QETProject::isContainerPath(output)) {
|
||||
QString error;
|
||||
if (!QETProject::writeContainer(doc, output, &error)) {
|
||||
err << "Cannot write '" << output << "': " << error << "\n";
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
QFile file(output);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
err << "Cannot open '" << output << "' for writing.\n";
|
||||
return 1;
|
||||
}
|
||||
QTextStream fout(&file);
|
||||
fout << doc.toString(4);
|
||||
file.close();
|
||||
}
|
||||
QTextStream fout(&file);
|
||||
fout << doc.toString(4);
|
||||
file.close();
|
||||
out << "Stamped " << fields.size() << " field(s) on "
|
||||
<< folios << " folio(s) -> " << output << "\n";
|
||||
return 0;
|
||||
|
||||
@@ -34,7 +34,10 @@ void setError(QString *error, const QString &text)
|
||||
if (error) *error = text;
|
||||
}
|
||||
|
||||
//An element on its own, as a .qet is written (QET::writeXmlFile())
|
||||
//An element on its own, written without adding indentation: the
|
||||
//whitespace text already in the document (QETProject keeps the symbol
|
||||
//definitions as they were read, spacing included) must come back as it
|
||||
//was, or the next save writes it differently. Parts are one line.
|
||||
QByteArray serialize(const QDomNode &node)
|
||||
{
|
||||
QDomDocument document;
|
||||
@@ -42,7 +45,7 @@ QByteArray serialize(const QDomNode &node)
|
||||
QStringLiteral("xml"),
|
||||
QStringLiteral("version=\"1.0\" encoding=\"UTF-8\"")));
|
||||
document.appendChild(document.importNode(node, true));
|
||||
return document.toString(4).toUtf8();
|
||||
return document.toString(-1).toUtf8();
|
||||
}
|
||||
|
||||
//A file name from a name chosen by the user: no folders, nothing a zip
|
||||
@@ -136,6 +139,30 @@ bool QetContainer::parse(const QByteArray &xml, QDomDocument *document, QString
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QetContainer::isZip
|
||||
A .qetz is recognised by what it holds, not its name: a crash backup
|
||||
of a .qetz project is plain XML, and a renamed file is still a zip.
|
||||
*/
|
||||
bool QetContainer::isZip(const QByteArray &content)
|
||||
{
|
||||
return content.startsWith("PK\x03\x04");
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QetContainer::readDocument
|
||||
The project document held in @p content: unzipped and joined for a
|
||||
.qetz, parsed for a .qet.
|
||||
*/
|
||||
bool QetContainer::readDocument(const QByteArray &content, QDomDocument *document, QString *error)
|
||||
{
|
||||
if (!isZip(content))
|
||||
return parse(content, document, error);
|
||||
QList<QetZip::Entry> entries;
|
||||
return QetZip::fromBytes(content, &entries, error)
|
||||
&& join(entries, document, error);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QetContainer::split
|
||||
@param project : the project document, as QETProject::toXml() makes it
|
||||
|
||||
@@ -54,6 +54,10 @@ namespace QetContainer
|
||||
|
||||
/// Parse @p xml as a .qet is read (QETProject::openFile)
|
||||
bool parse(const QByteArray &xml, QDomDocument *document, QString *error = nullptr);
|
||||
/// true if @p content is a zip, so a .qetz rather than a .qet
|
||||
bool isZip(const QByteArray &content);
|
||||
/// The project document in @p content, a .qet or a .qetz
|
||||
bool readDocument(const QByteArray &content, QDomDocument *document, QString *error = nullptr);
|
||||
}
|
||||
|
||||
#endif // QETCONTAINER_H
|
||||
|
||||
@@ -2669,8 +2669,8 @@ void projectDataBase::exportDb(projectDataBase *db,
|
||||
if (dir_.isEmpty()) {
|
||||
dir_ = QETApp::documentDir() % "/" % tr("untitled") % ".sqlite";
|
||||
} else {
|
||||
dir_.remove(".qet");
|
||||
dir_.append(".sqlite");
|
||||
const QFileInfo project_file(dir_);
|
||||
dir_ = project_file.path() % "/" % project_file.completeBaseName() % ".sqlite";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+15
-4
@@ -339,11 +339,16 @@ int ProjectView::tryClosingDiagrams()
|
||||
*/
|
||||
QString ProjectView::askUserForFilePath(bool assign) {
|
||||
// ask the user for a filepath in order to save the project
|
||||
const QString qet_filter = tr("Project QElectroTech (*.qet)", "filetypes allowed when saving a project file");
|
||||
const QString qetz_filter = tr("Zipped project QElectroTech (*.qetz)", "filetypes allowed when saving a project file");
|
||||
QString selected_filter = QETProject::isContainerPath(m_project->filePath()) ? qetz_filter : qet_filter;
|
||||
QString filepath = QFileDialog::getSaveFileName(
|
||||
this,
|
||||
tr("Save as", "dialog title"),
|
||||
m_project -> currentDir() + "/" + tr("untitled") + ".qet",
|
||||
tr("Project QElectroTech (*.qet)", "filetypes allowed when saving a project file")
|
||||
m_project -> currentDir() + "/" + tr("untitled")
|
||||
+ (selected_filter == qetz_filter ? ".qetz" : ".qet"),
|
||||
qet_filter + ";;" + qetz_filter,
|
||||
&selected_filter
|
||||
);
|
||||
|
||||
// if no filepath is provided, return an empty string
|
||||
@@ -359,9 +364,15 @@ QString ProjectView::askUserForFilePath(bool assign) {
|
||||
// extension at all whenever the portal didn't (bugtracker #270).
|
||||
// Stripping any existing suffix first and re-appending it once is
|
||||
// correct either way.
|
||||
if (filepath.endsWith(".qet", Qt::CaseInsensitive))
|
||||
// A .qetz is kept as typed, or chosen through its filter.
|
||||
const bool zipped = filepath.endsWith(".qetz", Qt::CaseInsensitive)
|
||||
|| (selected_filter == qetz_filter
|
||||
&& !filepath.endsWith(".qet", Qt::CaseInsensitive));
|
||||
if (filepath.endsWith(".qetz", Qt::CaseInsensitive))
|
||||
filepath.chop(5);
|
||||
else if (filepath.endsWith(".qet", Qt::CaseInsensitive))
|
||||
filepath.chop(4);
|
||||
filepath += ".qet";
|
||||
filepath += zipped ? ".qetz" : ".qet";
|
||||
|
||||
if (assign) {
|
||||
// assign the provided filepath to the currently edited project
|
||||
|
||||
@@ -1179,6 +1179,7 @@ QStringList QETApp::handledFileExtensions()
|
||||
static QStringList ext;
|
||||
if (!ext.count()) {
|
||||
ext << "qet";
|
||||
ext << "qetz";
|
||||
ext << "elmt";
|
||||
ext << QString(TITLEBLOCKS_FILE_EXTENSION).remove(QRegularExpression("^\\."));
|
||||
}
|
||||
|
||||
@@ -1745,7 +1745,7 @@ bool QETDiagramEditor::openProject()
|
||||
this,
|
||||
tr("Open a file"),
|
||||
open_dialog_dir.absolutePath(),
|
||||
tr("QElectroTech project(*.qet);;XML Files (*.xml);;All files (*)")
|
||||
tr("QElectroTech project(*.qet *.qetz);;XML Files (*.xml);;All files (*)")
|
||||
);
|
||||
if (filepath.isEmpty()) return(false);
|
||||
|
||||
|
||||
+50
-10
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
|
||||
#include "qetproject.h"
|
||||
#include "container/qetcontainer.h"
|
||||
|
||||
#include "ElementsCollection/xmlelementcollection.h"
|
||||
#include "autoNum/assignvariables.h"
|
||||
@@ -385,10 +386,20 @@ QETProject::ProjectState QETProject::openFile(QFile *file)
|
||||
load_timer.start();
|
||||
|
||||
bool opened_here = file->isOpen() ? false : true;
|
||||
if (!file->isOpen()
|
||||
&& !file->open(QIODevice::ReadOnly
|
||||
| QIODevice::Text)) {
|
||||
return FileOpenFailed;
|
||||
if (opened_here)
|
||||
{
|
||||
//A .qetz is a zip and is read as bytes: text mode would turn
|
||||
//its CR LF pairs into LF on Windows. A .qet is read as text,
|
||||
//as it always was.
|
||||
if (!file->open(QIODevice::ReadOnly)) {
|
||||
return FileOpenFailed;
|
||||
}
|
||||
if (!QetContainer::isZip(file->peek(4))) {
|
||||
file->close();
|
||||
if (!file->open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
return FileOpenFailed;
|
||||
}
|
||||
}
|
||||
}
|
||||
QFileInfo fi(*file);
|
||||
setFilePath(fi.absoluteFilePath());
|
||||
@@ -410,12 +421,12 @@ QETProject::ProjectState QETProject::openFile(QFile *file)
|
||||
// call .text() on themselves -- which is exactly where the bug was.
|
||||
// The option exists since Qt 6.5; older Qt always drops such nodes,
|
||||
// so there an all-whitespace value still reloads as "".
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
|
||||
if (!xml_project.setContent(content, QDomDocument::ParseOption::PreserveSpacingOnlyNodes))
|
||||
#else
|
||||
if (!xml_project.setContent(content))
|
||||
#endif
|
||||
// QetContainer::parse() does exactly that, for a .qet and for each part
|
||||
// of a .qetz alike.
|
||||
QString read_error;
|
||||
if (!QetContainer::readDocument(content, &xml_project, &read_error))
|
||||
{
|
||||
qWarning().noquote() << "Unable to read" << fi.fileName() << ":" << read_error;
|
||||
if(opened_here) {
|
||||
file->close();
|
||||
}
|
||||
@@ -1617,6 +1628,30 @@ bool QETProject::close()
|
||||
@see setFilePath()
|
||||
@return true if the project was successfully saved, else false
|
||||
*/
|
||||
/**
|
||||
@brief QETProject::isContainerPath
|
||||
@return true if @p path names a .qetz file: write() saves it zipped
|
||||
*/
|
||||
bool QETProject::isContainerPath(const QString &path)
|
||||
{
|
||||
return path.endsWith(QLatin1String(".qetz"), Qt::CaseInsensitive);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETProject::writeContainer
|
||||
Write the project document @p project to @p path as a .qetz: the same
|
||||
document, in parts, zipped (#1440). The file is replaced only once
|
||||
the whole archive is written.
|
||||
*/
|
||||
bool QETProject::writeContainer(const QDomDocument &project, const QString &path, QString *error)
|
||||
{
|
||||
const QList<QetZip::Entry> entries = QetContainer::split(
|
||||
project,
|
||||
QStringLiteral("QElectroTech %1").arg(QetVersion::currentVersion().toString()),
|
||||
error);
|
||||
return !entries.isEmpty() && QetZip::write(path, entries, error);
|
||||
}
|
||||
|
||||
QETResult QETProject::write()
|
||||
{
|
||||
// this operation requires a filepath
|
||||
@@ -1639,7 +1674,12 @@ QETResult QETProject::write()
|
||||
|
||||
QDomDocument xml_project(toXml());
|
||||
QString error_message;
|
||||
if (!QET::writeXmlFile(xml_project, m_file_path, &error_message))
|
||||
if (isContainerPath(m_file_path))
|
||||
{
|
||||
if (!writeContainer(xml_project, m_file_path, &error_message))
|
||||
return(error_message);
|
||||
}
|
||||
else if (!QET::writeXmlFile(xml_project, m_file_path, &error_message))
|
||||
return(error_message);
|
||||
|
||||
// The project has just been written to a writable file (e.g. saved to
|
||||
|
||||
@@ -345,6 +345,11 @@ class QETProject : public QObject
|
||||
void writeBackup();
|
||||
void init();
|
||||
ProjectState openFile(QFile *file);
|
||||
public:
|
||||
static bool isContainerPath(const QString &path);
|
||||
static bool writeContainer(const QDomDocument &project, const QString &path,
|
||||
QString *error = nullptr);
|
||||
private:
|
||||
static QUuid derivedUuid(const QByteArray &content);
|
||||
void refresh();
|
||||
|
||||
|
||||
@@ -921,7 +921,9 @@ QJsonObject LiveServer::snapshot(const QString &raw_path)
|
||||
if (info.exists()) return failure(QStringLiteral("%1 already exists").arg(raw_path));
|
||||
QDomDocument xml(project->toXml());
|
||||
QString error;
|
||||
if (!QET::writeXmlFile(xml, info.absoluteFilePath(), &error))
|
||||
if (QETProject::isContainerPath(info.absoluteFilePath())
|
||||
? !QETProject::writeContainer(xml, info.absoluteFilePath(), &error)
|
||||
: !QET::writeXmlFile(xml, info.absoluteFilePath(), &error))
|
||||
return failure(QStringLiteral("snapshot not written: %1").arg(error));
|
||||
return {{QStringLiteral("ok"), true}, {QStringLiteral("path"), info.absoluteFilePath()},
|
||||
{QStringLiteral("folio"), e->currentDiagramView()
|
||||
|
||||
@@ -408,7 +408,9 @@ bool QetScriptApi::save(const QString &output)
|
||||
|
||||
QDomDocument xml_doc(m_project->toXml());
|
||||
QString error_message;
|
||||
if (!QET::writeXmlFile(xml_doc, output, &error_message)) {
|
||||
if (QETProject::isContainerPath(output)
|
||||
? !QETProject::writeContainer(xml_doc, output, &error_message)
|
||||
: !QET::writeXmlFile(xml_doc, output, &error_message)) {
|
||||
log(QStringLiteral("qet.save: %1").arg(error_message));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "../../container/qetcontainer.h"
|
||||
#include "projectconfigpages.h"
|
||||
|
||||
#include "../autoNum/autonumschemecommand.h"
|
||||
@@ -1054,7 +1055,7 @@ void ProjectAutoNumConfigPage::importFromProject()
|
||||
this,
|
||||
tr("Import numbering from a project"),
|
||||
m_project->currentDir(),
|
||||
tr("Project QElectroTech (*.qet)"));
|
||||
tr("Project QElectroTech (*.qet *.qetz)"));
|
||||
if (path.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
@@ -1067,7 +1068,7 @@ void ProjectAutoNumConfigPage::importFromProject()
|
||||
}
|
||||
|
||||
QDomDocument doc;
|
||||
if (!doc.setContent(&file)) {
|
||||
if (!QetContainer::readDocument(file.readAll(), &doc)) {
|
||||
QMessageBox::warning(this, tr("Import not possible"),
|
||||
tr("%1 is not a valid QElectroTech project.")
|
||||
.arg(QFileInfo(path).fileName()));
|
||||
|
||||
@@ -4,13 +4,16 @@
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include "container/qetcontainer.h"
|
||||
|
||||
// A project document split into the parts of a .qetz, zipped, unzipped and
|
||||
// joined back is the document it was: every example, and every .qet in the
|
||||
// folder QET_CONTAINER_CORPUS names, if set. Whitespace-only text between
|
||||
// elements is indentation and is not compared; elsewhere it is (a
|
||||
// title-block value of " ", #973).
|
||||
// folder QET_CONTAINER_CORPUS names, if set -- exactly, whitespace text
|
||||
// included: QETProject writes the symbol definitions back as it read them,
|
||||
// spacing and all, so a part that came back re-indented would change the
|
||||
// next save.
|
||||
class tst_qetcontainer : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
@@ -18,16 +21,9 @@ class tst_qetcontainer : public QObject
|
||||
static QList<QDomNode> children(const QDomNode &node)
|
||||
{
|
||||
QList<QDomNode> all;
|
||||
bool has_element = false;
|
||||
for (QDomNode c = node.firstChild() ; !c.isNull() ; c = c.nextSibling()) {
|
||||
for (QDomNode c = node.firstChild() ; !c.isNull() ; c = c.nextSibling())
|
||||
all << c;
|
||||
has_element |= c.isElement();
|
||||
}
|
||||
if (!has_element) return all;
|
||||
QList<QDomNode> kept;
|
||||
for (const QDomNode &c : all)
|
||||
if (!(c.isText() && c.nodeValue().trimmed().isEmpty())) kept << c;
|
||||
return kept;
|
||||
return all;
|
||||
}
|
||||
|
||||
// empty when equal, else where they differ
|
||||
@@ -106,6 +102,36 @@ private slots:
|
||||
QVERIFY2(d.isEmpty(), qPrintable(d));
|
||||
}
|
||||
|
||||
// Splitting and joining adds no whitespace text. QETProject keeps the
|
||||
// symbol definitions as it read them and writes them back as they
|
||||
// are, so indentation a part gained would change the next save
|
||||
// (forum attachments 1328, 1978, 2057, 3003, 3004 did, with indented
|
||||
// parts). Here: a document without any whitespace-only text, as
|
||||
// QETProject::toXml() builds one, must come back without any.
|
||||
void noWhitespaceAdded()
|
||||
{
|
||||
QDomDocument original;
|
||||
QVERIFY(original.setContent(read(QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"))));
|
||||
auto whitespace_nodes = [](const QDomDocument &d) {
|
||||
int n = 0;
|
||||
std::function<void(const QDomNode &)> walk = [&](const QDomNode &node) {
|
||||
for (QDomNode c = node.firstChild() ; !c.isNull() ; c = c.nextSibling()) {
|
||||
if (c.isText() && c.nodeValue().trimmed().isEmpty()) ++n;
|
||||
walk(c);
|
||||
}
|
||||
};
|
||||
walk(d);
|
||||
return n;
|
||||
};
|
||||
QCOMPARE(whitespace_nodes(original), 0);
|
||||
QString error;
|
||||
const QList<QetZip::Entry> entries = QetContainer::split(original, QString(), &error);
|
||||
QVERIFY2(!entries.isEmpty(), qPrintable(error));
|
||||
QDomDocument back;
|
||||
QVERIFY2(QetContainer::join(entries, &back, &error), qPrintable(error));
|
||||
QCOMPARE(whitespace_nodes(back), 0);
|
||||
}
|
||||
|
||||
// The folio pictures leave the folios, once each
|
||||
void picturesAreFiles()
|
||||
{
|
||||
|
||||
@@ -30,9 +30,12 @@ class tst_resaveunchanged : public QObject
|
||||
|
||||
// --resave @p in to a new file, in a sandbox of its own (so a running
|
||||
// QElectroTech cannot answer instead); returns the new file's path.
|
||||
QString resave(const QString &in)
|
||||
QString resave(const QString &in, const QString &suffix = QStringLiteral(".qet"))
|
||||
{
|
||||
const QString out = m_dir.filePath(QStringLiteral("out%1.qet").arg(m_run));
|
||||
//Always p.qet, in its own folder: an untitled project takes its
|
||||
//file name as title, so two names would make two projects.
|
||||
QDir().mkpath(m_dir.filePath(QStringLiteral("run%1").arg(m_run)));
|
||||
const QString out = m_dir.filePath(QStringLiteral("run%1/p").arg(m_run) + suffix);
|
||||
const QString home = m_dir.filePath(QStringLiteral("home%1").arg(m_run++));
|
||||
QDir().mkpath(home);
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
@@ -282,6 +285,38 @@ private slots:
|
||||
QVERIFY2(read(saved).contains(b), "the arrow without a terminal was not kept");
|
||||
}
|
||||
|
||||
// Saved as a .qetz (the zipped project, #1440) and that saved again as a
|
||||
// .qet, a project is what it is saved straight as a .qet: the same
|
||||
// file but for when and where it was saved. The examples with pictures
|
||||
// (weneedpolonez), tables and links (industrial) and several title
|
||||
// blocks (m_000).
|
||||
void containerSavesTheSameProject_data()
|
||||
{
|
||||
QTest::addColumn<QString>("project");
|
||||
for (const char *name : {"Projet_vierge.qet", "industrial.qet", "m_000.qet",
|
||||
"weneedpolonez-Polonez_MR89_wiring_diagram.qet"})
|
||||
QTest::newRow(name) << QStringLiteral(QET_EXAMPLES_DIR "/") + QLatin1String(name);
|
||||
}
|
||||
void containerSavesTheSameProject()
|
||||
{
|
||||
QFETCH(QString, project);
|
||||
const QString straight = resave(project);
|
||||
QVERIFY2(!straight.isEmpty(), "--resave as .qet failed");
|
||||
const QString zipped = resave(project, QStringLiteral(".qetz"));
|
||||
QVERIFY2(!zipped.isEmpty(), "--resave as .qetz failed");
|
||||
QVERIFY2(read(zipped).startsWith("PK\x03\x04"), "the .qetz is not a zip");
|
||||
QVERIFY2(read(zipped).size() < read(straight).size() / 2, "the .qetz is not smaller");
|
||||
const QString back = resave(zipped);
|
||||
QVERIFY2(!back.isEmpty(), "opening the .qetz failed");
|
||||
|
||||
auto without_save_stamps = [](QByteArray xml) {
|
||||
static const QRegularExpression stamp(QStringLiteral(
|
||||
"\\s*<property [^>]*name=\"saved(filename|filepath|filenamedir|date|date-us|date-eu|time)\"[^>]*>[^<]*</property>"));
|
||||
return QString::fromUtf8(xml).remove(stamp);
|
||||
};
|
||||
QCOMPARE(without_save_stamps(read(back)), without_save_stamps(read(straight)));
|
||||
}
|
||||
|
||||
// A title-block value that is a single space is kept through two saves
|
||||
// (#973), and a value with accents comes back as it went in.
|
||||
void singleSpaceValueKept()
|
||||
|
||||
Reference in New Issue
Block a user