Skip to content

Commit 0b0b693

Browse files
committed
Encode line breaks as <br/> tags for file saving
1 parent 42dd45f commit 0b0b693

File tree

14 files changed

+528
-29
lines changed

14 files changed

+528
-29
lines changed

src/engraving/dom/textbase.cpp

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1815,6 +1815,32 @@ void TextBase::createBlocks(LayoutData* ldata) const
18151815
cursor.setRow(0);
18161816
cursor.setColumn(0);
18171817

1818+
auto insertNewBlock = [](LayoutData* ldata, TextCursor& cursor) {
1819+
if (ldata->rows() <= cursor.row()) {
1820+
ldata->blocks.push_back(TextBlock());
1821+
}
1822+
1823+
if (cursor.row() < ldata->rows()) {
1824+
if (ldata->blocks.at(cursor.row()).fragments().size() == 0) {
1825+
ldata->blocks[cursor.row()].insertEmptyFragmentIfNeeded(&cursor); // used to preserve the Font size of the line (font info is held in TextFragments, see PR #5881)
1826+
}
1827+
1828+
ldata->blocks[cursor.row()].setEol(true);
1829+
}
1830+
1831+
cursor.setRow(cursor.row() + 1);
1832+
cursor.setColumn(0);
1833+
if (ldata->rows() <= cursor.row()) {
1834+
ldata->blocks.push_back(TextBlock());
1835+
}
1836+
1837+
if (cursor.row() < ldata->rows()) {
1838+
if (ldata->blocks.at(cursor.row()).fragments().size() == 0) {
1839+
ldata->blocks[cursor.row()].insertEmptyFragmentIfNeeded(&cursor); // an empty fragment may be needed on either side of the newline
1840+
}
1841+
}
1842+
};
1843+
18181844
int state = 0;
18191845
String token;
18201846
String sym;
@@ -1829,29 +1855,7 @@ void TextBase::createBlocks(LayoutData* ldata) const
18291855
state = 2;
18301856
token.clear();
18311857
} else if (c == '\n') {
1832-
if (ldata->rows() <= cursor.row()) {
1833-
ldata->blocks.push_back(TextBlock());
1834-
}
1835-
1836-
if (cursor.row() < ldata->rows()) {
1837-
if (ldata->blocks.at(cursor.row()).fragments().size() == 0) {
1838-
ldata->blocks[cursor.row()].insertEmptyFragmentIfNeeded(&cursor); // used to preserve the Font size of the line (font info is held in TextFragments, see PR #5881)
1839-
}
1840-
1841-
ldata->blocks[cursor.row()].setEol(true);
1842-
}
1843-
1844-
cursor.setRow(cursor.row() + 1);
1845-
cursor.setColumn(0);
1846-
if (ldata->rows() <= cursor.row()) {
1847-
ldata->blocks.push_back(TextBlock());
1848-
}
1849-
1850-
if (cursor.row() < ldata->rows()) {
1851-
if (ldata->blocks.at(cursor.row()).fragments().size() == 0) {
1852-
ldata->blocks[cursor.row()].insertEmptyFragmentIfNeeded(&cursor); // an empty fragment may be needed on either side of the newline
1853-
}
1854-
}
1858+
insertNewBlock(ldata, cursor);
18551859
} else {
18561860
if (symState) {
18571861
sym += c;
@@ -1887,6 +1891,8 @@ void TextBase::createBlocks(LayoutData* ldata) const
18871891
} else {
18881892
LOGD("unknown symbol <%s>", muPrintable(sym));
18891893
}
1894+
} else if (token == "br/") {
1895+
insertNewBlock(ldata, cursor);
18901896
}
18911897
} else {
18921898
token += c;
@@ -2256,7 +2262,7 @@ String TextBase::genText(const LayoutData* ldata) const
22562262
fmt = format;
22572263
}
22582264
if (block.eol()) {
2259-
text += Char::LineFeed;
2265+
text += String(u"<br/>");
22602266
}
22612267
}
22622268
while (!xmlNesting.empty()) {
@@ -3725,7 +3731,7 @@ String TextBase::stripText(bool removeStyle, bool removeSize, bool removeFace) c
37253731
fmt = format;
37263732
}
37273733
if (block.eol()) {
3728-
_txt += Char::LineFeed;
3734+
_txt += String(u"<br/>");
37293735
}
37303736
}
37313737
while (!xmlNesting.empty()) {

src/engraving/rw/xmlreader.cpp

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,11 +169,12 @@ double XmlReader::readDouble(double min, double max)
169169

170170
void XmlReader::htmlToString(int level, String* s)
171171
{
172+
bool selfClosing = noChildren();
172173
*s += u'<' + String::fromAscii(name().ascii());
173174
for (const Attribute& a : attributes()) {
174175
*s += u' ' + String::fromAscii(a.name.ascii()) + u"=\"" + a.value + u'\"';
175176
}
176-
*s += u'>';
177+
*s += selfClosing ? u"/>" : u">";
177178
++level;
178179
for (;;) {
179180
XmlStreamReader::TokenType t = readNext();
@@ -182,7 +183,9 @@ void XmlReader::htmlToString(int level, String* s)
182183
htmlToString(level, s);
183184
break;
184185
case XmlStreamReader::EndElement:
185-
*s += u"</" + String::fromAscii(name().ascii()) + u'>';
186+
if (!selfClosing) {
187+
*s += u"</" + String::fromAscii(name().ascii()) + u'>';
188+
}
186189
--level;
187190
return;
188191
case XmlStreamReader::Characters:

src/engraving/tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ set(MODULE_TEST_SRC
9292
${CMAKE_CURRENT_LIST_DIR}/changevisibility_tests.cpp
9393
${CMAKE_CURRENT_LIST_DIR}/scoreutils_tests.cpp
9494
${CMAKE_CURRENT_LIST_DIR}/voiceswitching_tests.cpp
95+
${CMAKE_CURRENT_LIST_DIR}/engraving_xml_tests.cpp
9596

9697
${CMAKE_CURRENT_LIST_DIR}/mocks/engravingconfigurationmock.h
9798
)

src/engraving/tests/copypaste_data/copypaste_parts-ref.mscx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@
115115
<tempo>1.333333</tempo>
116116
<followText>1</followText>
117117
<eid>H_H</eid>
118-
<text><sym>metNoteQuarterUp</sym><font face="Edwin"></font> = 80</text>
118+
<text><sym>metNoteQuarterUp</sym><font face="Edwin"/> = 80</text>
119119
</Tempo>
120120
<Spanner type="Pedal">
121121
<Pedal>
@@ -922,7 +922,7 @@
922922
<followText>1</followText>
923923
<eid>EC_EC</eid>
924924
<linkedTo>H_H</linkedTo>
925-
<text><sym>metNoteQuarterUp</sym><font face="Edwin"></font> = 80</text>
925+
<text><sym>metNoteQuarterUp</sym><font face="Edwin"/> = 80</text>
926926
</Tempo>
927927
<Spanner type="Pedal">
928928
<Pedal>
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* SPDX-License-Identifier: GPL-3.0-only
3+
* MuseScore-Studio-CLA-applies
4+
*
5+
* MuseScore Studio
6+
* Music Composition & Notation
7+
*
8+
* Copyright (C) 2025 MuseScore Limited
9+
*
10+
* This program is free software: you can redistribute it and/or modify
11+
* it under the terms of the GNU General Public License version 3 as
12+
* published by the Free Software Foundation.
13+
*
14+
* This program is distributed in the hope that it will be useful,
15+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17+
* GNU General Public License for more details.
18+
*
19+
* You should have received a copy of the GNU General Public License
20+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
21+
*/
22+
23+
#include <gtest/gtest.h>
24+
#include "engraving/rw/xmlreader.h"
25+
#include "engraving/rw/xmlwriter.h"
26+
#include "thirdparty/kors_logger/src/log_base.h"
27+
#include "types/bytearray.h"
28+
#include "io/buffer.h"
29+
30+
using namespace mu;
31+
using namespace mu::engraving;
32+
using namespace muse;
33+
using namespace muse::io;
34+
35+
class Engraving_XMLTests : public ::testing::Test
36+
{
37+
};
38+
39+
TEST_F(Engraving_XMLTests, readHTML)
40+
{
41+
ByteArray data;
42+
Buffer buf(&data);
43+
static const String XML_TEXT_REF = u"<tag1><br/></tag1>";
44+
static const String XML_TEXT_VERBOSE = u"<tag1><br></br></tag1>";
45+
46+
// Write
47+
{
48+
buf.open(IODevice::WriteOnly);
49+
XmlWriter xml(&buf);
50+
xml.startDocument();
51+
xml.startElement("parent");
52+
53+
xml.writeXml(u"xmlTag", XML_TEXT_REF);
54+
xml.writeXml(u"xmlTag", XML_TEXT_VERBOSE);
55+
56+
xml.endElement();
57+
58+
EXPECT_NE(data.size(), 0);
59+
buf.close();
60+
61+
LOGI() << buf.data().constChar();
62+
}
63+
64+
// Read
65+
{
66+
buf.open(IODevice::ReadOnly);
67+
XmlReader xml(&buf);
68+
69+
EXPECT_TRUE(xml.readNextStartElement());
70+
EXPECT_EQ(xml.name(), "parent");
71+
72+
EXPECT_TRUE(xml.readNextStartElement());
73+
EXPECT_EQ(xml.name(), "xmlTag");
74+
String xmlTag = xml.readXml();
75+
LOGI() << xmlTag;
76+
77+
// Expect that <br></br> is condensed to <br/>
78+
EXPECT_TRUE(xml.readNextStartElement());
79+
EXPECT_EQ(xml.name(), "xmlTag");
80+
String xmlTagVerbose = xml.readXml();
81+
LOGI() << xmlTagVerbose;
82+
EXPECT_EQ(xmlTag, XML_TEXT_REF);
83+
}
84+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<museScore version="4.60">
3+
<Score>
4+
<eid>iBLuOHjPASI_QT4wvnLWMk</eid>
5+
<Division>480</Division>
6+
<Style>
7+
<spatium>1.74978</spatium>
8+
</Style>
9+
<showInvisible>1</showInvisible>
10+
<showUnprintable>1</showUnprintable>
11+
<showFrames>1</showFrames>
12+
<showMargins>0</showMargins>
13+
<open>1</open>
14+
<metaTag name="arranger"></metaTag>
15+
<metaTag name="composer">Composer / arranger</metaTag>
16+
<metaTag name="copyright"></metaTag>
17+
<metaTag name="lyricist"></metaTag>
18+
<metaTag name="movementNumber"></metaTag>
19+
<metaTag name="movementTitle"></metaTag>
20+
<metaTag name="source"></metaTag>
21+
<metaTag name="subtitle">Subtitle</metaTag>
22+
<metaTag name="translator"></metaTag>
23+
<metaTag name="workNumber"></metaTag>
24+
<metaTag name="workTitle">Untitled score</metaTag>
25+
<Order id="orchestra">
26+
<name>Orchestra</name>
27+
<instrument id="flute">
28+
<family id="flutes">Flutes</family>
29+
</instrument>
30+
<section id="woodwind" brackets="true" barLineSpan="true" thinBrackets="true">
31+
<family>flutes</family>
32+
<family>oboes</family>
33+
<family>clarinets</family>
34+
<family>saxophones</family>
35+
<family>bassoons</family>
36+
<unsorted group="woodwinds"/>
37+
</section>
38+
<section id="brass" brackets="true" barLineSpan="true" thinBrackets="true">
39+
<family>horns</family>
40+
<family>trumpets</family>
41+
<family>cornets</family>
42+
<family>flugelhorns</family>
43+
<family>trombones</family>
44+
<family>tubas</family>
45+
<unsorted group="brass"/>
46+
</section>
47+
<section id="timpani" brackets="true" barLineSpan="true" thinBrackets="true">
48+
<family>timpani</family>
49+
</section>
50+
<section id="percussion" brackets="true" barLineSpan="true" thinBrackets="true">
51+
<family>keyboard-percussion</family>
52+
<unsorted group="pitched-percussion"/>
53+
<family>drums</family>
54+
<family>unpitched-metal-percussion</family>
55+
<family>unpitched-wooden-percussion</family>
56+
<family>other-percussion</family>
57+
<unsorted group="unpitched-percussion"/>
58+
</section>
59+
<family>keyboards</family>
60+
<family>harps</family>
61+
<family>organs</family>
62+
<family>synths</family>
63+
<unsorted/>
64+
<soloists/>
65+
<section id="voices" brackets="true" barLineSpan="false" thinBrackets="true">
66+
<family>voices</family>
67+
<family>voice-groups</family>
68+
</section>
69+
<section id="strings" brackets="true" barLineSpan="true" thinBrackets="true">
70+
<family>orchestral-strings</family>
71+
</section>
72+
</Order>
73+
<Part id="1">
74+
<Staff>
75+
<eid>arh0Jm6otYD_HsfG596XOWB</eid>
76+
<StaffType group="pitched">
77+
<name>stdNormal</name>
78+
</StaffType>
79+
</Staff>
80+
<trackName>Flute</trackName>
81+
<Instrument id="flute">
82+
<longName>Flute</longName>
83+
<shortName>Fl.</shortName>
84+
<trackName>Flute</trackName>
85+
<minPitchP>59</minPitchP>
86+
<maxPitchP>98</maxPitchP>
87+
<minPitchA>60</minPitchA>
88+
<maxPitchA>93</maxPitchA>
89+
<instrumentId>wind.flutes.flute</instrumentId>
90+
<Channel>
91+
<program value="73"/>
92+
</Channel>
93+
</Instrument>
94+
</Part>
95+
<Staff id="1">
96+
<Measure>
97+
<eid>jDRK9Oj9N4C_DuKduF3zQYF</eid>
98+
<voice>
99+
<KeySig>
100+
<eid>ZtCEsE0hO9C_4gCv+F9/+1J</eid>
101+
<concertKey>0</concertKey>
102+
</KeySig>
103+
<TimeSig>
104+
<eid>qwnJM7hpnoH_l1RMPBD2dLH</eid>
105+
<sigN>4</sigN>
106+
<sigD>4</sigD>
107+
</TimeSig>
108+
<StaffText>
109+
<eid>z0BF5d3QUKN_z0BF5d3QUKN</eid>
110+
<text><b><i><s><font size="17"/>aaa<br/><br/>bbb</s></i></b></text>
111+
</StaffText>
112+
<Rest>
113+
<eid>RBw86gy1d4G_y0BF5d3QUKN</eid>
114+
<durationType>measure</durationType>
115+
<duration>4/4</duration>
116+
</Rest>
117+
</voice>
118+
</Measure>
119+
</Staff>
120+
</Score>
121+
</museScore>

0 commit comments

Comments
 (0)