1 //=============================================================================
2 //  MuseScore
3 //  Music Composition & Notation
4 //
5 //  Copyright (C) 2018 Werner Schweer
6 //
7 //  This program is free software; you can redistribute it and/or modify
8 //  it under the terms of the GNU General Public License version 2
9 //  as published by the Free Software Foundation and appearing in
10 //  the file LICENCE.GPL
11 //=============================================================================
12 
13 #include "scorediff.h"
14 
15 #include "duration.h"
16 #include "measure.h"
17 #include "score.h"
18 #include "staff.h"
19 #include "xml.h"
20 
21 #include "dtl/dtl.hpp"
22 
23 #include <algorithm>
24 #include <utility>
25 
26 namespace Ms {
27 
28 //---------------------------------------------------------
29 //   MscxModeDiff
30 //---------------------------------------------------------
31 
32 class MscxModeDiff {
33       static constexpr const char* tagRegExpStr = "</?(?<name>[A-z_][A-z_0-9\\-.:]*)( [A-z_0-9\\-.:\\s=\"]*)?/?>";
34       const QRegularExpression tagRegExp;
35 
36       enum TagType { START_TAG, END_TAG };
37       struct Tag {
38             int line;
39             TagType type;
40             QString name;
41 
TagMs::MscxModeDiff::Tag42             Tag(int l, TagType t, const QString& n) : line(l), type(t), name(n) {}
43             };
44 
45       static DiffType fromDtlDiffType(dtl::edit_t dtlType);
46 
47       void adjustSemanticsMscx(std::vector<TextDiff>&);
48       int adjustSemanticsMscxOneDiff(std::vector<TextDiff>& diffs, int index);
49       int nextDiffOnShiftIndex(const std::vector<TextDiff>& diffs, int index, bool down);
50       static QString getOuterLines(const QString& str, int lines, bool start);
51       bool assessShiftDiff(const std::vector<TextDiff>&, const std::vector<Tag>& extraTags, int index, int lines);
52       int performShiftDiff(std::vector<TextDiff>&, int index, int lines);
53       void readMscx(const QString& xml, std::vector<Tag>& extraTags);
54       void handleTag(int line, TagType type, const QString& name, std::vector<Tag>& extraTags);
55 
56    public:
57       MscxModeDiff();
58 
59       std::vector<TextDiff> lineModeDiff(const QString& s1, const QString& s2);
60       std::vector<TextDiff> mscxModeDiff(const QString& s1, const QString& s2);
61       };
62 
63 //---------------------------------------------------------
64 //   TextDiffParser
65 //    Used by ScoreDiff class during parsing MSCX text diff
66 //---------------------------------------------------------
67 
68 class TextDiffParser {
69       const int iScore;
70       const int iOtherScore;
71 
72       int tagLevel = 0;
73       int currentDiffTagLevel = 0;
74       std::vector<const ScoreElement*> contextsStack;
75       std::vector<bool> tagIsElement;
76       const ScoreElement* lastElementEnded = nullptr;
77       QString markupContext;
78 
79       BaseDiff* handleToken(const QXmlStreamReader& r, const ScoreElement* newElement, bool saveDiff);
insideDiffTag() const80       bool insideDiffTag() const { return currentDiffTagLevel != 0; }
81 
82    public:
83       TextDiffParser(int iScore);
84 
85       void makeDiffs(const QString& mscx, const std::vector<std::pair<const ScoreElement*, QString>>& elements, const std::vector<TextDiff>& textDiffs, std::vector<BaseDiff*>& diffs);
86       };
87 
88 //---------------------------------------------------------
89 //   MscxModeDiff::MscxModeDiff
90 //---------------------------------------------------------
91 
MscxModeDiff()92 MscxModeDiff::MscxModeDiff()
93    : tagRegExp(tagRegExpStr)
94       {}
95 
96 //---------------------------------------------------------
97 //   MscxModeDiff::fromDtlDiffType
98 //---------------------------------------------------------
99 
fromDtlDiffType(dtl::edit_t dtlType)100 DiffType MscxModeDiff::fromDtlDiffType(dtl::edit_t dtlType)
101       {
102       switch(dtlType) {
103             case dtl::SES_DELETE:
104                   return DiffType::DELETE;
105             case dtl::SES_COMMON:
106                   return DiffType::EQUAL;
107             case dtl::SES_ADD:
108                   return DiffType::INSERT;
109             }
110       Q_ASSERT(false); // dtlType must have one of the values handled above.
111       return DiffType::EQUAL;
112       }
113 
114 //---------------------------------------------------------
115 //   MscxModeDiff::lineModeDiff
116 //---------------------------------------------------------
117 
lineModeDiff(const QString & s1,const QString & s2)118 std::vector<TextDiff> MscxModeDiff::lineModeDiff(const QString& s1, const QString& s2)
119       {
120       // type declarations for dtl library
121       typedef QStringRef elem;
122       typedef std::pair<elem, dtl::elemInfo> sesElem;
123       typedef std::vector<sesElem> sesElemVec;
124 
125       // QVector does not contain range constructor used inside dtl
126       // so we have to convert to std::vector.
127       std::vector<QStringRef> lines1 = s1.splitRef('\n').toStdVector();
128       std::vector<QStringRef> lines2 = s2.splitRef('\n').toStdVector();
129       dtl::Diff<QStringRef, std::vector<QStringRef>> diff(lines1, lines2);
130 
131       diff.compose();
132 
133       const sesElemVec changes = diff.getSes().getSequence();
134       std::vector<TextDiff> diffs;
135       int line[2][2] {{1, 1}, {1, 1}}; // for correct assigning line numbers to
136                                        // DELETE and INSERT diffs we need to
137                                        // count lines separately for these diff
138                                        // types (EQUAL can use both counters).
139 
140       for (const sesElem& ch : changes) {
141             DiffType type = fromDtlDiffType(ch.second.type);
142             const int iThis = (type == DiffType::DELETE) ? 0 : 1; // for EQUAL doesn't matter
143 
144             if (diffs.empty() || diffs.back().type != type) {
145                   if (!diffs.empty()) {
146                         // sync line numbers
147                         DiffType prevType = diffs.back().type;
148                         const int prevThis = (prevType == DiffType::DELETE) ? 0 : 1;
149                         const int prevOther = (prevThis == 1) ? 0 : 1;
150                         if (prevType == DiffType::EQUAL)
151                               std::copy_n(line[prevThis], 2, line[prevOther]);
152                         else
153                               line[prevThis][prevOther] = line[prevOther][prevOther];
154 
155                         if (type == DiffType::EQUAL) {
156                               const int iOther = (iThis == 1) ? 0 : 1;
157                               line[iThis][iOther] = line[iOther][iOther];
158                               }
159                         }
160 
161                   diffs.emplace_back();
162                   TextDiff& d = diffs.back();
163                   d.type = type;
164                   std::copy_n(line[iThis], 2, d.start);
165                   d.end[0] = line[iThis][0] - 1; // equal line numbers would mean an actual change in that line.
166                   d.end[1] = line[iThis][1] - 1;
167                   }
168 
169             TextDiff& d = diffs.back();
170             d.end[iThis] = (line[iThis][iThis]++);
171             d.text[iThis].append(ch.first).append('\n');
172             if (type == DiffType::EQUAL) {
173                   const int iOther = (iThis == 1) ? 0 : 1;
174                   d.end[iOther] = (line[iThis][iOther]++);
175                   d.text[iOther].append(ch.first).append('\n');
176                   }
177             }
178 
179       return diffs;
180       }
181 
182 //---------------------------------------------------------
183 //   MscxModeDiff::mscxModeDiff
184 //---------------------------------------------------------
185 
mscxModeDiff(const QString & s1,const QString & s2)186 std::vector<TextDiff> MscxModeDiff::mscxModeDiff(const QString& s1, const QString& s2)
187       {
188       std::vector<TextDiff> diffs = lineModeDiff(s1, s2);
189       adjustSemanticsMscx(diffs);
190       return diffs;
191       }
192 
193 //---------------------------------------------------------
194 //   MscxModeDiff::adjustSemanticsMscx
195 //---------------------------------------------------------
196 
adjustSemanticsMscx(std::vector<TextDiff> & diffs)197 void MscxModeDiff::adjustSemanticsMscx(std::vector<TextDiff>& diffs)
198       {
199       for (unsigned i = 0; i < diffs.size(); ++i)
200             i = adjustSemanticsMscxOneDiff(diffs, i);
201       }
202 
203 //---------------------------------------------------------
204 //   MscxModeDiff::adjustSemanticsMscx
205 //    Returns new index of the given diff
206 //---------------------------------------------------------
207 
adjustSemanticsMscxOneDiff(std::vector<TextDiff> & diffs,int index)208 int MscxModeDiff::adjustSemanticsMscxOneDiff(std::vector<TextDiff>& diffs, int index)
209       {
210       TextDiff* diff = &diffs[index];
211       int iScore;
212       switch(diff->type) {
213             case DiffType::EQUAL:
214             case DiffType::REPLACE:
215                   // TODO: split a REPLACE diff, though they should not be here
216                   return index;
217             case DiffType::INSERT:
218                   iScore = 1;
219                   break;
220             case DiffType::DELETE:
221             default:
222                   iScore = 0;
223                   break;
224             }
225 
226       std::vector<Tag> extraTags;
227       readMscx(diff->text[iScore], extraTags);
228 
229       if (extraTags.empty())
230             return index; // good, nothing to adjust
231 
232       auto firstStartExtra = extraTags.begin();
233       auto lastStartExtra = extraTags.begin();
234       auto firstEndExtra = extraTags.begin();
235       auto lastEndExtra = extraTags.begin();
236       for (auto tag = extraTags.begin(); tag != extraTags.end(); ++tag) {
237             TagType type = tag->type;
238             switch(type) {
239                   case START_TAG:
240                         if (firstStartExtra->type != START_TAG)
241                               firstStartExtra = tag;
242                         lastStartExtra = tag;
243                         break;
244                   case END_TAG:
245                         if (firstStartExtra->type != END_TAG)
246                               firstEndExtra = tag;
247                         lastEndExtra = tag;
248                         break;
249                   default:
250                         break;
251                   }
252             }
253 
254       // Try to shift current diff up and down and see whether this helps
255       int lines = lastEndExtra->line - firstEndExtra->line + 1;
256       if (assessShiftDiff(diffs, extraTags, index, lines))
257             return performShiftDiff(diffs, index, lines);
258       else {
259             lines = - (lastStartExtra->line - firstStartExtra->line + 1);
260             if (assessShiftDiff(diffs, extraTags, index, lines))
261                   return performShiftDiff(diffs, index, lines);
262             }
263 
264       return index;
265       }
266 
267 //---------------------------------------------------------
268 //   MscxModeDiff::assessShiftDiff
269 //    Returns true if shift of the diff with the given
270 //    index is possible and there would be no extra tags
271 //    left after such a shift.
272 //---------------------------------------------------------
273 
assessShiftDiff(const std::vector<TextDiff> & diffs,const std::vector<Tag> & origExtraTags,int index,int lines)274 bool MscxModeDiff::assessShiftDiff(const std::vector<TextDiff>& diffs, const std::vector<Tag>& origExtraTags, int index, int lines)
275       {
276       if (lines == 0)
277             return diffs.empty();
278       const bool down = (lines > 0);
279       const int nextDiffIdx = nextDiffOnShiftIndex(diffs, index, down);
280       if (nextDiffIdx == index)
281             return false;
282 
283       const TextDiff& diff = diffs[index];
284       const TextDiff& nextDiff = diffs[nextDiffIdx];
285 
286       Q_ASSERT(diff.type == DiffType::DELETE || diff.type == DiffType::INSERT);
287       const int iScore = (diff.type == DiffType::DELETE ? 0 : 1);
288 
289       const QString& diffTxt = diff.text[iScore];
290       const QString& nextTxt = nextDiff.text[iScore];
291       const QString diffChunk = getOuterLines(diffTxt, lines, down);
292       const QString nextChunk = getOuterLines(nextTxt, lines, down);
293 
294       if (diffChunk == nextChunk) {
295             std::vector<Tag> extraTags(origExtraTags);
296             std::vector<Tag> diffExtraTags;
297             readMscx(diffChunk, diffExtraTags);
298 
299             // merge diff chunk reading result with the result
300             // for the whole chunk
301             while (!diffExtraTags.empty()) {
302                   auto tag = down ? diffExtraTags.begin() : (diffExtraTags.end() - 1);
303 
304                   if (extraTags.size() < 2) // would normally erase 2 tags
305                         return false;
306 
307                   auto firstExtra = extraTags.begin();
308                   if ((firstExtra->type == END_TAG)
309                         && (firstExtra->name == tag->name))
310                         extraTags.erase(firstExtra);
311                   else
312                         return false;
313                   auto lastExtra = extraTags.end() - 1;
314                   if ((lastExtra->type == START_TAG)
315                         && (lastExtra->name == tag->name))
316                         extraTags.erase(lastExtra);
317                   else
318                         return false;
319 
320                   diffExtraTags.erase(tag);
321                   }
322 
323             if (extraTags.empty()) {
324                   // We have succeeded
325                   return true;
326                   }
327             }
328       return false;
329       }
330 
331 //---------------------------------------------------------
332 //   MscxModeDiff::performShiftDiff
333 //    Perform a shift of the diff with the given index and
334 //    return a new index of the diff.
335 //---------------------------------------------------------
336 
performShiftDiff(std::vector<TextDiff> & diffs,int index,int lines)337 int MscxModeDiff::performShiftDiff(std::vector<TextDiff>& diffs, int index, int lines)
338       {
339       if (lines == 0)
340             return index;
341       const bool down = (lines > 0);
342       const int absLines = qAbs(lines);
343       const int nextDiffIdx = nextDiffOnShiftIndex(diffs, index, down);
344       if (nextDiffIdx == index)
345             return index;
346 
347       TextDiff& diff = diffs[index];
348       TextDiff& nextDiff = diffs[nextDiffIdx];
349 
350       Q_ASSERT(diff.type == DiffType::DELETE || diff.type == DiffType::INSERT);
351       const int iScore = (diff.type == DiffType::DELETE ? 0 : 1);
352       QString& diffTxt = diff.text[iScore];
353 
354       const QString diffChunk = getOuterLines(diffTxt, absLines, down);
355 
356       Q_ASSERT(nextDiff.type == DiffType::EQUAL);
357       int chunkStart[2]; // line numbers of diffChunk in two scores
358       int chunkEnd[2];
359       if (down) {
360             std::copy(diff.start, diff.start + 2, chunkStart);
361             chunkEnd[0] = diff.start[0] + absLines - 1;
362             chunkEnd[1] = diff.start[1] + absLines - 1;
363             }
364       else {
365             chunkStart[0] = diff.end[0] - (absLines - 1);
366             chunkStart[1] = diff.end[1] - (absLines - 1);
367             std::copy(diff.end, diff.end + 2, chunkEnd);
368             }
369 
370       if (down) {
371             diffTxt.remove(0, diffChunk.size());
372             diffTxt.append(diffChunk);
373             nextDiff.text[0].remove(0, diffChunk.size());
374             nextDiff.text[1].remove(0, diffChunk.size());
375             }
376       else {
377             diffTxt.chop(diffChunk.size());
378             diffTxt.prepend(diffChunk);
379             nextDiff.text[0].chop(diffChunk.size());
380             nextDiff.text[1].chop(diffChunk.size());
381             }
382       // shift line numbers: needed both for shifted diff and for diffs
383       // between the shifted diff and nextDiff
384       const int inc = down ? 1 : -1;
385       for (int i = index; i != nextDiffIdx; i += inc) {
386             TextDiff& d = diffs[i];
387             Q_UNUSED(d);
388             Q_ASSERT(d.type != DiffType::EQUAL); // nextDiff should be the first EQUAL diff in that direction
389             diff.start[0] += lines;
390             diff.end[0] += lines;
391             diff.start[1] += lines;
392             diff.end[1] += lines;
393             }
394       int* nextChunkLines = down ? nextDiff.start : nextDiff.end;
395       nextChunkLines[0] += lines;
396       nextChunkLines[1] += lines;
397 
398       TextDiff eqDiff;
399       eqDiff.type = DiffType::EQUAL;
400       eqDiff.text[0] = diffChunk;
401       eqDiff.text[1] = diffChunk;
402       std::copy(chunkStart, chunkStart + 2, eqDiff.start);
403       std::copy(chunkEnd, chunkEnd + 2, eqDiff.end);
404 
405       const int prevDiffIdx = index + (down ? -1 : 1);
406       bool merged = false;
407       if (diffs[prevDiffIdx].type == DiffType::EQUAL)
408             merged = diffs[prevDiffIdx].merge(eqDiff);
409 
410       if (!merged) {
411             const int insertIdx = down ? index : (index + 1);
412             diffs.insert(diffs.begin() + insertIdx, eqDiff);
413             if (down)
414                   ++index;
415             }
416 
417       return index;
418       }
419 
420 //---------------------------------------------------------
421 //   MscxModeDiff::getOuterLines
422 //---------------------------------------------------------
423 
getOuterLines(const QString & str,int lines,bool start)424 QString MscxModeDiff::getOuterLines(const QString& str, int lines, bool start) {
425       lines = qAbs(lines);
426       const int secIdxStart = start ? 0 : (-1 - (lines - 1));
427       const int secIdxEnd = start ? (lines - 1) : -1;
428       constexpr auto secFlags = QString::SectionIncludeTrailingSep | QString::SectionSkipEmpty;
429       return str.section('\n', secIdxStart, secIdxEnd, secFlags);
430       }
431 
432 //---------------------------------------------------------
433 //   MscxModeDiff::nextDiffOnShiftIndex
434 //---------------------------------------------------------
435 
nextDiffOnShiftIndex(const std::vector<TextDiff> & diffs,int index,bool down)436 int MscxModeDiff::nextDiffOnShiftIndex(const std::vector<TextDiff>& diffs, int index, bool down)
437       {
438       const DiffType diffType = diffs[index].type;
439       Q_ASSERT(diffType == DiffType::DELETE || diffType == DiffType::INSERT);
440       int nextDiffIdx = index + (down ? 1 : -1);
441       while (nextDiffIdx >= 0 && nextDiffIdx < int(diffs.size())) {
442             const TextDiff& d = diffs[nextDiffIdx];
443             if (d.type == diffType) {
444                   // Two INSERT or DELETE diffs in a row, probably shouldn't happen
445                   return index;
446                   }
447             if (d.type == DiffType::EQUAL)
448                   return nextDiffIdx;
449             // REPLACE is not here, the other type can be skipped
450             Q_ASSERT(d.type != DiffType::REPLACE);
451             nextDiffIdx += (down ? 1 : -1);
452             }
453       return index;
454       }
455 
456 //---------------------------------------------------------
457 //   MscxModeDiff::readMscx
458 //---------------------------------------------------------
459 
readMscx(const QString & xml,std::vector<Tag> & extraTags)460 void MscxModeDiff::readMscx(const QString& xml, std::vector<Tag>& extraTags)
461       {
462       int line = 0;
463       int nextLineIndex = xml.indexOf('\n');
464       QRegularExpressionMatchIterator m = tagRegExp.globalMatch(xml);
465       while (m.hasNext()) {
466             QRegularExpressionMatch match = m.next();
467             QStringRef tag(match.capturedRef("name"));
468             while ((match.capturedStart("name") > nextLineIndex) && (nextLineIndex > 0)) {
469                   ++line;
470                   nextLineIndex = xml.indexOf('\n', nextLineIndex + 1);
471                   }
472             if (match.capturedRef().startsWith("</")) {
473                   // end element
474                   handleTag(line, END_TAG, tag.toString(), extraTags);
475                   if (tag == "Tuplet") {
476                         // special case: <Tuplet> / <endTuplet/> pair
477                         handleTag(line, START_TAG, "__tupletBound", extraTags);
478                         }
479                   }
480             else if (match.capturedRef().endsWith("/>")) {
481                   // empty element: do nothing
482                   if (tag == "endTuplet") {
483                         // special case: <Tuplet> / <endTuplet/> pair
484                         handleTag(line, END_TAG, "__tupletBound", extraTags);
485                         }
486                   }
487             else {
488                   // start element
489                   handleTag(line, START_TAG, tag.toString(), extraTags);
490                   }
491             }
492       }
493 
494 //---------------------------------------------------------
495 //   MscxModeDiff::handleTag
496 //---------------------------------------------------------
497 
handleTag(int line,TagType type,const QString & name,std::vector<Tag> & extraTags)498 void MscxModeDiff::handleTag(int line, TagType type, const QString& name, std::vector<Tag>& extraTags)
499       {
500       switch(type) {
501             case START_TAG:
502                   extraTags.emplace_back(line, type, name);
503                   break;
504             case END_TAG:
505                   {
506                   auto lastTag = extraTags.empty() ? nullptr : &extraTags.back();
507                   if (lastTag
508                      && lastTag->type == START_TAG && lastTag->name == name)
509                         extraTags.pop_back();
510                   else
511                         extraTags.emplace_back(line, type, name);
512                   }
513                   break;
514             }
515       }
516 
517 //---------------------------------------------------------
518 //   TextDiffParser
519 //---------------------------------------------------------
520 
TextDiffParser(int iScore)521 TextDiffParser::TextDiffParser(int iScore)
522    : iScore(iScore), iOtherScore(iScore == 1 ? 0 : 1)
523       {
524       contextsStack.push_back(nullptr);
525       }
526 
527 //---------------------------------------------------------
528 //   TextDiffParser::makeDiffs
529 //---------------------------------------------------------
530 
makeDiffs(const QString & mscx,const std::vector<std::pair<const ScoreElement *,QString>> & elements,const std::vector<TextDiff> & textDiffs,std::vector<BaseDiff * > & diffs)531 void TextDiffParser::makeDiffs(const QString& mscx, const std::vector<std::pair<const ScoreElement*, QString>>& elements, const std::vector<TextDiff>& textDiffs, std::vector<BaseDiff*>& diffs)
532       {
533       QXmlStreamReader r(mscx);
534       r.readNext(); // read first tag
535       auto textDiff = textDiffs.begin();
536       auto textDiffEnd = textDiffs.end();
537       auto nextElement = elements.begin();
538       auto nextElementEnd = elements.end();
539       while (!r.atEnd()) {
540             // Scroll to the correct diff to be handled now
541             while (textDiff != textDiffEnd
542                && textDiff->end[iScore] < r.lineNumber()) {
543                   // Save ContextChange when leaving equal chunk
544                   if (textDiff->type == DiffType::EQUAL) {
545                         ContextChange* c = new ContextChange;
546                         c->type = textDiff->type;
547                         c->textDiff = &(*textDiff);
548                         c->ctx[iScore] = contextsStack.back();
549                         c->ctx[iOtherScore] = nullptr;
550                         c->before[iScore] = lastElementEnded;
551                         c->before[iOtherScore] = nullptr;
552                         diffs.push_back(c);
553                         }
554 
555                   ++textDiff;
556                   }
557             if (textDiff == textDiffEnd)
558                   break;
559 
560             bool saveDiff = false;
561             if (!insideDiffTag()
562                && (textDiff->type == DiffType::DELETE || textDiff->type == DiffType::INSERT)
563                ) {
564                   const int iDiffScore = (textDiff->type == DiffType::DELETE) ? 0 : 1;
565                   if (iScore == iDiffScore)
566                         saveDiff = true;
567                   }
568 
569             const ScoreElement* newElement = nullptr;
570             if (r.isStartElement()) {
571                   if (nextElement != nextElementEnd
572                      && nextElement->second == r.name())
573                         newElement = (nextElement++)->first;
574 
575                   ++tagLevel;
576                   tagIsElement.push_back(newElement);
577                   }
578 
579             BaseDiff* diff = handleToken(r, newElement, saveDiff);
580 
581             if (diff) {
582                   diff->type = textDiff->type;
583                   diff->textDiff = &(*textDiff);
584                   diff->before[iScore] = lastElementEnded;
585                   diff->before[iOtherScore] = nullptr;
586                   diffs.push_back(diff);
587 
588                   if (saveDiff) {
589                         // It is assumed that diff items are created in handleToken
590                         // only for start tags
591                         Q_ASSERT(r.isStartElement());
592                         ItemType type = diff->itemType();
593                         if (type == ItemType::ELEMENT || type == ItemType::PROPERTY) {
594                               currentDiffTagLevel = tagLevel;
595                               if (type == ItemType::ELEMENT && contextsStack.back()->isSpanner()) {
596                                     // Consider <Spanner> tag as the current differing tag
597                                     --currentDiffTagLevel;
598                                     }
599                               }
600                         }
601                   }
602 
603             if (r.isEndElement()) {
604                   if (currentDiffTagLevel == tagLevel)
605                         currentDiffTagLevel = 0;
606                   --tagLevel;
607                   tagIsElement.pop_back();
608                   }
609 
610             r.readNext();
611             }
612       if (r.hasError())
613             qWarning("TextDiffParser::makeDiffs: error while reading MSCX output: %s", r.errorString().toLatin1().constData());
614       }
615 
616 //---------------------------------------------------------
617 //   TextDiffParser::handleToken
618 //    Returns nullptr or a newly allocated BaseDiff with
619 //    properly filled context and fields that are unique to
620 //    descendants of BaseDiff (e.g. el[2] for ElementDiff).
621 //    Type and textDiff are left unfilled.
622 //---------------------------------------------------------
623 
handleToken(const QXmlStreamReader & r,const ScoreElement * newElement,bool saveDiff)624 BaseDiff* TextDiffParser::handleToken(const QXmlStreamReader& r, const ScoreElement* newElement, bool saveDiff)
625       {
626       if (!r.isStartElement() && !r.isEndElement())
627             return nullptr;
628       BaseDiff* diff = nullptr;
629       const QStringRef tag(r.name());
630 
631       if (r.isStartElement()) {
632             if (newElement) {
633                   if (saveDiff) {
634                         ElementDiff* d = new ElementDiff;
635                         diff = d;
636                         d->ctx[iScore] = contextsStack.back();
637                         d->el[iScore] = newElement;
638                         d->el[iOtherScore] = nullptr;
639                         }
640                   contextsStack.push_back(newElement);
641                   }
642             else if (tag == "Spanner")
643                   markupContext = tag.toString();
644             else if (saveDiff) {
645                   const ScoreElement* ctx = contextsStack.back();
646                   if (markupContext == "Spanner" && !(ctx && ctx->isSpanner())) {
647                         // some property inside the end of a spanner
648                         }
649                   else if (tag == "prev" || tag == "next") {
650                         // Ignore these tags
651                         }
652                   else if (tag == "location" || tag == "voice") {
653                         if (ctx && ctx->isMeasure()) {
654                               MarkupDiff* d = new MarkupDiff;
655                               diff = d;
656                               d->ctx[iScore] = ctx;
657                               d->name = tag.toString();
658                               const ScoreElement* widerCtx = *(contextsStack.end() - 2);
659                               if (widerCtx && widerCtx->isStaff())
660                                     d->info = toStaff(widerCtx)->idx();
661                               }
662                         }
663                   else {
664                         Pid propId = ctx ? ctx->propertyId(tag) : Pid::END;
665                         if (propId != Pid::END) {
666                               PropertyDiff* d = new PropertyDiff;
667                               diff = d;
668                               d->ctx[iScore] = contextsStack.back();
669                               d->pid = propId;
670                               }
671                         else {
672                               MarkupDiff* d = new MarkupDiff;
673                               diff = d;
674                               d->ctx[iScore] = contextsStack.back();
675                               d->name = tag.toString();
676                               if (tag == "metaTag")
677                                     d->info = r.attributes().value("name").toString();
678                               }
679                         }
680                   }
681             }
682       else { // end element
683             if ((tagIsElement.back() && !contextsStack.back()->isSpanner())
684                || (tag == "Spanner" && contextsStack.back()->isSpanner())
685                ) {
686                   lastElementEnded = contextsStack.back();
687                   contextsStack.pop_back();
688                   }
689             if (!markupContext.isEmpty() && tag == markupContext)
690                   markupContext.clear();
691             }
692 
693       if (diff)
694             diff->ctx[iOtherScore] = nullptr;
695       return diff;
696       }
697 
698 //---------------------------------------------------------
699 //   lineNumberSort
700 //---------------------------------------------------------
701 
lineNumberSort(BaseDiff * d1,BaseDiff * d2)702 static bool lineNumberSort(BaseDiff* d1, BaseDiff* d2)
703       {
704       const TextDiff* t1 = d1->textDiff;
705       const TextDiff* t2 = d2->textDiff;
706       return ((t1->end[0] < t2->end[0]) || (t1->end[1] < t2->end[1]));
707       }
708 
709 //---------------------------------------------------------
710 //   positionSort
711 //---------------------------------------------------------
712 
positionSort(BaseDiff * d1,BaseDiff * d2)713 static bool positionSort(BaseDiff* d1, BaseDiff* d2)
714       {
715       return (d1->afrac(0) < d2->afrac(0) || d1->afrac(1) < d2->afrac(1));
716       }
717 
718 //---------------------------------------------------------
719 //   scoreToMscx
720 //---------------------------------------------------------
721 
scoreToMscx(Score * s,XmlWriter & xml)722 static QString scoreToMscx(Score* s, XmlWriter& xml)
723       {
724       QString mscx;
725       xml.setString(&mscx, QIODevice::WriteOnly);
726       xml.setRecordElements(true);
727       s->write(xml, /* onlySelection */ false);
728       xml.flush();
729       return mscx;
730       }
731 
732 //---------------------------------------------------------
733 //   ScoreDiff
734 //    s1, s2 are scores to compare.
735 //    ScoreDiff does NOT take ownership of the scores.
736 //---------------------------------------------------------
737 
ScoreDiff(Score * s1,Score * s2,bool textDiffOnly)738 ScoreDiff::ScoreDiff(Score* s1, Score* s2, bool textDiffOnly)
739    : _s1(s1), _s2(s2), _textDiffOnly(textDiffOnly)
740       {
741       update();
742       }
743 
744 //---------------------------------------------------------
745 //   makeDiffs
746 //---------------------------------------------------------
747 
makeDiffs(const QString & mscx1,const QString & mscx2,const XmlWriter & xml1,const XmlWriter & xml2,const std::vector<TextDiff> & textDiffs,std::vector<BaseDiff * > & diffs)748 static void makeDiffs(const QString& mscx1, const QString& mscx2, const XmlWriter& xml1, const XmlWriter& xml2, const std::vector<TextDiff>& textDiffs, std::vector<BaseDiff*>& diffs)
749       {
750       TextDiffParser p1(0);
751       p1.makeDiffs(mscx1, xml1.elements(), textDiffs, diffs);
752       TextDiffParser p2(1);
753       p2.makeDiffs(mscx2, xml2.elements(), textDiffs, diffs);
754 
755       std::stable_sort(diffs.begin(), diffs.end(), lineNumberSort);
756 
757       // Fill correct contexts for diffs
758       const ScoreElement* lastCtx[2] {};
759       const ScoreElement* lastBefore[2] {};
760       for (BaseDiff* diff : diffs) {
761             for (int i = 0; i < 2; ++i) {
762                   if (diff->ctx[i])
763                         lastCtx[i] = diff->ctx[i];
764                   else
765                         diff->ctx[i] = lastCtx[i];
766 
767                   if (diff->before[i])
768                         lastBefore[i] = diff->before[i];
769                   else
770                         diff->before[i] = lastBefore[i];
771                   }
772             }
773 
774       // Filter out EQUAL diffs and dummy ContextChange items
775       std::vector<BaseDiff*> filteredDiffs;
776       for (BaseDiff* diff : diffs) {
777             if ((diff->itemType() == ItemType::CONTEXTCHANGE)
778                || (diff->type == DiffType::EQUAL))
779                   delete diff;
780             else
781                   filteredDiffs.push_back(diff);
782             }
783       std::swap(diffs, filteredDiffs);
784       }
785 
786 //---------------------------------------------------------
787 //   ScoreDiff::update
788 //    Updates the diff according to the current state of
789 //    the compared scores.
790 //---------------------------------------------------------
791 
update()792 void ScoreDiff::update()
793       {
794       if (updated())
795             return;
796 
797       qDeleteAll(_diffs);
798       _diffs.clear();
799       _textDiffs.clear();
800       qDeleteAll(_mergedTextDiffs);
801       _mergedTextDiffs.clear();
802 
803       XmlWriter xml1(_s1);
804       XmlWriter xml2(_s2);
805       QString mscx1(scoreToMscx(_s1, xml1));
806       QString mscx2(scoreToMscx(_s2, xml2));
807 
808       _textDiffs = MscxModeDiff().mscxModeDiff(mscx1, mscx2);
809 
810       if (!_textDiffOnly) {
811             makeDiffs(mscx1, mscx2, xml1, xml2, _textDiffs, _diffs);
812 
813             processMarkupDiffs();
814             mergeInsertDeleteDiffs();
815             mergeElementDiffs();
816             editPropertyDiffs();
817 
818             std::stable_sort(_diffs.begin(), _diffs.end(), positionSort);
819             }
820 
821       _scoreState1 = _s1->state();
822       _scoreState2 = _s2->state();
823       }
824 
825 //---------------------------------------------------------
826 //   ~ScoreDiff
827 //---------------------------------------------------------
828 
~ScoreDiff()829 ScoreDiff::~ScoreDiff()
830       {
831       qDeleteAll(_mergedTextDiffs);
832       qDeleteAll(_diffs);
833       }
834 
835 //---------------------------------------------------------
836 //   deleteDiffs
837 //---------------------------------------------------------
838 
deleteDiffs(std::vector<BaseDiff * > & diffsList,const std::vector<BaseDiff * > & toDelete)839 static void deleteDiffs(std::vector<BaseDiff*>& diffsList, const std::vector<BaseDiff*>& toDelete)
840       {
841       for (BaseDiff* diff : toDelete) {
842             for (auto it = diffsList.begin(); it != diffsList.end(); ++it) {
843                   if (*it == diff) {
844                         diffsList.erase(it);
845                         break;
846                         }
847                   }
848             delete diff;
849             }
850       }
851 
852 //---------------------------------------------------------
853 //   measureToMscx
854 //---------------------------------------------------------
855 
measureToMscx(const Measure * m,XmlWriter & xml,int staff)856 static QString measureToMscx(const Measure* m, XmlWriter& xml, int staff)
857       {
858       QString mscx;
859       xml.setString(&mscx, QIODevice::WriteOnly);
860       xml.setRecordElements(true);
861       m->write(xml, staff, false, false);
862       xml.flush();
863       return mscx;
864       }
865 
866 //---------------------------------------------------------
867 //   measureInfo
868 //---------------------------------------------------------
869 
870 struct MeasureInfo {
871       const Measure* m1;
872       const Measure* m2;
873       int staff;
874 
MeasureInfoMs::MeasureInfo875       MeasureInfo(const Measure* m1, const Measure* m2, int staff) : m1(m1), m2(m2), staff(staff) {}
876 
operator ==Ms::MeasureInfo877       bool operator==(const MeasureInfo& other) { return m1 == other.m1 && m2 == other.m2 && staff == other.staff; }
operator !=Ms::MeasureInfo878       bool operator!=(const MeasureInfo& other) { return !(*this == other); }
879       };
880 
881 //---------------------------------------------------------
882 //   ScoreDiff::processMarkupDiffs
883 //    Find MarkupDiffs and convert them to the appropriate
884 //    diffs of other types if needed.
885 //---------------------------------------------------------
886 
processMarkupDiffs()887 void ScoreDiff::processMarkupDiffs()
888       {
889       std::vector<BaseDiff*> abandonedDiffs;
890       std::vector<MeasureInfo> measuresToProcess;
891       Pid pids[] { Pid::TRACK, Pid::POSITION }; // list of pids to be accepted on measures processing
892       for (BaseDiff* diff : _diffs) {
893             if (diff->itemType() != ItemType::MARKUP)
894                   continue;
895 
896             MarkupDiff* d = static_cast<MarkupDiff*>(diff);
897             const QString& name = d->name;
898 
899             if (name == "voice" || name == "location") {
900                   if (d->ctx[0]->isMeasure() && d->ctx[1]->isMeasure()) {
901                         MeasureInfo m(toMeasure(d->ctx[0]), toMeasure(d->ctx[1]), d->info.toInt());
902                         bool add = true;
903                         for (auto& mScheduled : measuresToProcess) {
904                               if (mScheduled == m) {
905                                     add = false;
906                                     break;
907                                     }
908                               }
909                         if (add)
910                               measuresToProcess.push_back(m);
911                         abandonedDiffs.push_back(d);
912                         }
913                   }
914             }
915 
916       std::vector<BaseDiff*> newDiffs;
917       for (auto& m : measuresToProcess) {
918             XmlWriter xml1(m.m1->score());
919             xml1.setWriteTrack(true);
920             xml1.setWritePosition(true);
921             QString mscx1 = measureToMscx(m.m1, xml1, m.staff);
922 
923             XmlWriter xml2(m.m2->score());
924             xml2.setWriteTrack(true);
925             xml2.setWritePosition(true);
926             QString mscx2 = measureToMscx(m.m2, xml2, m.staff);
927 
928             std::vector<TextDiff> textDiffs = MscxModeDiff().mscxModeDiff(mscx1, mscx2);
929             makeDiffs(mscx1, mscx2, xml1, xml2, textDiffs, newDiffs);
930             }
931 
932       for (BaseDiff* newDiff : newDiffs) {
933             bool save = false;
934             if (newDiff->itemType() == ItemType::PROPERTY) {
935                   PropertyDiff* pDiff = static_cast<PropertyDiff*>(newDiff);
936                   for (Pid pid : pids) {
937                         if (pDiff->pid == pid) {
938                               save = true;
939                               break;
940                               }
941                         }
942                   if (pDiff->pid == Pid::TRACK)
943                         pDiff->pid = Pid::VOICE;
944 
945                   if (pDiff->ctx[0]->isMeasure() || pDiff->ctx[1]->isMeasure())
946                         save = false;
947                   }
948 
949 
950             if (save) {
951                   newDiff->textDiff = nullptr;
952                   _diffs.push_back(newDiff);
953                   }
954             else
955                   delete newDiff;
956             }
957 
958       deleteDiffs(_diffs, abandonedDiffs);
959       }
960 
961 //---------------------------------------------------------
962 //   ScoreDiff::mergeInsertDeleteDiffs
963 //    Merge INSERT and DELETE diffs to REPLACE diffs where
964 //    possible.
965 //---------------------------------------------------------
966 
mergeInsertDeleteDiffs()967 void ScoreDiff::mergeInsertDeleteDiffs()
968       {
969       const ScoreElement* lastCtx1 = _s1;
970       const ScoreElement* lastCtx2 = _s2;
971       std::vector<BaseDiff*> diffsToMerge;
972       std::vector<BaseDiff*> abandonedDiffs;
973       for (BaseDiff* diff : _diffs) {
974             if ((diff->ctx[0] != lastCtx1) || (diff->ctx[1] != lastCtx2)) {
975                   diffsToMerge.clear();
976                   lastCtx1 = diff->ctx[0];
977                   lastCtx2 = diff->ctx[1];
978                   }
979             bool merge = false;
980             for (auto diff2It = diffsToMerge.begin(); diff2It != diffsToMerge.end(); ++diff2It) {
981                   BaseDiff* diff2 = (*diff2It);
982                   if (diff->type == DiffType::DELETE) {
983                         if ((diff2->type == DiffType::INSERT) && diff->sameItem(*diff2))
984                               merge = true;
985                         }
986                   else if (diff->type == DiffType::INSERT) {
987                         if ((diff2->type == DiffType::DELETE) && diff->sameItem(*diff2))
988                               merge = true;
989                         }
990 
991                   if (merge) {
992                         // To preserve the correct order of diffs, we should
993                         // leave diff2 in place since it appeared earlier in
994                         // the _diffs list.
995                         diff2->type = DiffType::REPLACE;
996 
997                         if (diff->textDiff && diff2->textDiff) {
998                               TextDiff* mergedTextDiff = new TextDiff(*diff2->textDiff);
999                               mergedTextDiff->merge(*diff->textDiff);
1000                               _mergedTextDiffs.push_back(mergedTextDiff);
1001                               diff2->textDiff = mergedTextDiff;
1002                               }
1003                         else if (!diff2->textDiff)
1004                               diff2->textDiff = diff->textDiff;
1005 
1006                         int idx1 = (diff->type == DiffType::DELETE) ? 0 : 1;
1007                         switch(diff2->itemType()) {
1008                               case ItemType::ELEMENT:
1009                                     {
1010                                     ElementDiff* d = static_cast<ElementDiff*>(diff);
1011                                     ElementDiff* d2 = static_cast<ElementDiff*>(diff2);
1012                                     // d2->el[idx2] is already filled
1013                                     d2->el[idx1] = d->el[idx1];
1014                                     }
1015                                     break;
1016                               case ItemType::PROPERTY:
1017                               case ItemType::MARKUP:
1018                                     // nothing to fill
1019                                     break;
1020                               case ItemType::CONTEXTCHANGE:
1021                                     break;
1022                               }
1023 
1024                         abandonedDiffs.push_back(diff);
1025                         diffsToMerge.erase(diff2It);
1026                         break; // loop on diffsToMerge
1027                         }
1028                   }
1029 
1030             if (!merge)
1031                   diffsToMerge.push_back(diff);
1032             }
1033 
1034       deleteDiffs(_diffs, abandonedDiffs);
1035       }
1036 
1037 //---------------------------------------------------------
1038 //   ScoreDiff::mergeElementDiffs
1039 //    Merge element diffs with equal elements
1040 //---------------------------------------------------------
1041 
mergeElementDiffs()1042 void ScoreDiff::mergeElementDiffs()
1043       {
1044       std::map<const ScoreElement*, ElementDiff*> elementDiffs;
1045       std::vector<BaseDiff*> abandonedDiffs;
1046       for (BaseDiff* diff : _diffs) {
1047             if (diff->itemType() != ItemType::ELEMENT)
1048                   continue;
1049 
1050             ElementDiff* elDiff = static_cast<ElementDiff*>(diff);
1051             auto foundDiffIt1 = elementDiffs.find(elDiff->el[0]);
1052             auto foundDiffIt2 = elementDiffs.find(elDiff->el[1]);
1053             ElementDiff* foundDiff = nullptr;
1054             if (foundDiffIt1 != elementDiffs.end())
1055                   foundDiff = foundDiffIt1->second;
1056             else if (foundDiffIt2 != elementDiffs.end())
1057                   foundDiff = foundDiffIt2->second;
1058 
1059             if (foundDiff && foundDiff->type == elDiff->type
1060                && foundDiff->el[0] == elDiff->el[0]
1061                && foundDiff->el[1] == elDiff->el[1]) {
1062                   for (int i = 0; i < 2; ++i) {
1063                         const ScoreElement* se = foundDiff->el[i];
1064                         if (!se)
1065                               continue;
1066                         if (!se->isMeasure() && se->isElement()) {
1067                               const Element* m = toElement(se)->findMeasure();
1068                               if (m)
1069                                     foundDiff->ctx[i] = m;
1070                               else
1071                                     foundDiff->ctx[i] = se->score();
1072                               }
1073                         else
1074                               foundDiff->ctx[i] = se->score();
1075                         }
1076                   abandonedDiffs.push_back(elDiff);
1077                   }
1078             else {
1079                   for (int i = 0; i < 2; ++i) {
1080                         if (const ScoreElement* se = elDiff->el[i])
1081                               elementDiffs[se] = elDiff;
1082                         }
1083                   }
1084             }
1085 
1086       deleteDiffs(_diffs, abandonedDiffs);
1087       }
1088 
1089 //---------------------------------------------------------
1090 //   ScoreDiff::editPropertyDiffs
1091 //    Merge property diffs with equal elements, edit some
1092 //    diffs if necessary
1093 //---------------------------------------------------------
1094 
editPropertyDiffs()1095 void ScoreDiff::editPropertyDiffs()
1096       {
1097       std::multimap<const ScoreElement*, PropertyDiff*> propertyDiffs;
1098       std::vector<BaseDiff*> abandonedDiffs;
1099       for (BaseDiff* diff : _diffs) {
1100             if (diff->itemType() != ItemType::PROPERTY)
1101                   continue;
1102 
1103             PropertyDiff* pDiff = static_cast<PropertyDiff*>(diff);
1104 
1105             if (pDiff->pid == Pid::TPC1 || pDiff->pid == Pid::TPC2) {
1106                   // Special case: "tpc" and "pitch" properties are partially
1107                   // overlapped so we will mark tpc changes as pitch changes
1108                   pDiff->pid = Pid::PITCH;
1109                   }
1110 
1111             auto foundRange = propertyDiffs.equal_range(pDiff->ctx[0]);
1112             bool merged = false;
1113             for (auto it = foundRange.first; it != foundRange.second; ++it) {
1114                   PropertyDiff* foundDiff = it->second;
1115                   if (foundDiff && foundDiff->type == pDiff->type
1116                      && foundDiff->ctx[0] == pDiff->ctx[0]
1117                      && foundDiff->ctx[1] == pDiff->ctx[1]
1118                      && foundDiff->pid == pDiff->pid
1119                      ) {
1120                         abandonedDiffs.push_back(pDiff);
1121                         merged = true;
1122                         break;
1123                         }
1124                   }
1125 
1126             if (!merged)
1127                   propertyDiffs.emplace(pDiff->ctx[0], pDiff);
1128             }
1129 
1130       deleteDiffs(_diffs, abandonedDiffs);
1131       }
1132 
1133 //---------------------------------------------------------
1134 //   ScoreDiff::equal
1135 //---------------------------------------------------------
1136 
equal() const1137 bool ScoreDiff::equal() const
1138       {
1139       for (const TextDiff& td : _textDiffs) {
1140             if (td.type != DiffType::EQUAL)
1141                   return false;
1142             }
1143       return true;
1144       }
1145 
1146 //---------------------------------------------------------
1147 //   ScoreDiff::rawDiff
1148 //---------------------------------------------------------
1149 
rawDiff(bool skipEqual) const1150 QString ScoreDiff::rawDiff(bool skipEqual) const
1151       {
1152       QStringList list;
1153       for (const TextDiff& td : _textDiffs) {
1154             if (!skipEqual || td.type != DiffType::EQUAL)
1155                   list.push_back(td.toString(true));
1156             }
1157       return list.join('\n');
1158       }
1159 
1160 //---------------------------------------------------------
1161 //   ScoreDiff::userDiff
1162 //---------------------------------------------------------
1163 
userDiff() const1164 QString ScoreDiff::userDiff() const
1165       {
1166       QStringList list;
1167       for (const BaseDiff* d : _diffs)
1168             list.push_back(d->toString());
1169       return list.join('\n');
1170       }
1171 
1172 //---------------------------------------------------------
1173 //   TextDiff::merge
1174 //---------------------------------------------------------
1175 
merge(const TextDiff & other)1176 bool TextDiff::merge(const TextDiff& other)
1177       {
1178       if (type == other.type) {
1179             if (other.end[0] == (start[0] - 1) && other.end[1] == (start[1] - 1)) {
1180                   start[0] = other.start[0];
1181                   start[1] = other.start[1];
1182                   text[0].prepend(other.text[0]);
1183                   text[1].prepend(other.text[1]);
1184                   }
1185             else if ((end[0] + 1) == other.start[0] && (end[1] + 1) == other.start[1]) {
1186                   end[0] = other.end[0];
1187                   end[1] = other.end[1];
1188                   text[0].append(other.text[0]);
1189                   text[1].append(other.text[1]);
1190                   }
1191             else {
1192                   qWarning("TextDiff:merge: invalid argument: wrong line numbers");
1193                   return false;
1194                   }
1195             }
1196       else if ((type == DiffType::INSERT && other.type == DiffType::DELETE)
1197          || (type == DiffType::DELETE && other.type == DiffType::INSERT)
1198          ) {
1199             type = DiffType::REPLACE;
1200             const int iOther = (other.type == DiffType::DELETE) ? 0 : 1;
1201             start[iOther] = other.start[iOther];
1202             end[iOther] = other.end[iOther];
1203             text[iOther] = other.text[iOther];
1204             }
1205       else {
1206             qWarning("TextDiff:merge: invalid argument: wrong types");
1207             return false;
1208             }
1209 
1210       return true;
1211       }
1212 
1213 //---------------------------------------------------------
1214 //   addLinePrefix
1215 //---------------------------------------------------------
1216 
addLinePrefix(const QString & str,const QString & prefix)1217 static QString addLinePrefix(const QString& str, const QString& prefix)
1218       {
1219       if (prefix.isEmpty())
1220             return str;
1221       QVector<QStringRef> lines = str.splitRef('\n');
1222       if (lines.back().isEmpty())
1223             lines.pop_back();
1224       QStringList processedLines;
1225       for (QStringRef& line : lines)
1226             processedLines.push_back(QString(prefix).append(line));
1227       return processedLines.join('\n');
1228       }
1229 
1230 //---------------------------------------------------------
1231 //   TextDiff::toString
1232 //    type argument allows to define which part of diff
1233 //    should be printed. For example, it allows to print
1234 //    only deleted chunk for REPLACE diff item.
1235 //---------------------------------------------------------
1236 
toString(DiffType dt,bool prefixLines) const1237 QString TextDiff::toString(DiffType dt, bool prefixLines) const
1238       {
1239       if (dt == DiffType::REPLACE) {
1240             QStringList l;
1241             l.push_back(toString(DiffType::DELETE, prefixLines));
1242             l.push_back(toString(DiffType::INSERT, prefixLines));
1243             return l.join('\n');
1244             }
1245 
1246       int idx = (dt == DiffType::INSERT) ? 1 : 0;
1247       const char* prefix = (dt == DiffType::INSERT) ? ">" : "<";
1248 
1249       QString lines[2];
1250       for (int i = 0; i < 2; ++i) {
1251             if ((i != idx && dt != DiffType::EQUAL)
1252                   || end[i] <= start[i]
1253                   ) {
1254                   lines[i] = QString::number(start[i]);
1255                   }
1256             else
1257                   lines[i] = QString("%1--%2")
1258                         .arg(start[i], end[i]);
1259             }
1260 
1261       return QString("@%1;%2\n%3")
1262             .arg(lines[0], lines[1],
1263                   prefixLines ? addLinePrefix(text[idx], prefix) : text[idx]);
1264       }
1265 
1266 //---------------------------------------------------------
1267 //   BaseDiff::sameItem
1268 //---------------------------------------------------------
1269 
sameItem(const BaseDiff & other) const1270 bool BaseDiff::sameItem(const BaseDiff& other) const
1271       {
1272       if (itemType() != other.itemType()
1273          || ctx[0] != other.ctx[0] || ctx[1] != other.ctx[1])
1274             return false;
1275       if (textDiff == other.textDiff)
1276             return true;
1277       else if (!textDiff || !other.textDiff)
1278             return false;
1279       return (textDiff->start[0] == other.textDiff->start[0]
1280          && textDiff->start[1] == other.textDiff->start[1]
1281          );
1282       }
1283 
1284 //---------------------------------------------------------
1285 //   BaseDiff::afrac
1286 //    Returns position of the changed chunk
1287 //---------------------------------------------------------
1288 
afrac(int score) const1289 Fraction BaseDiff::afrac(int score) const
1290       {
1291       Q_ASSERT(score == 0 || score == 1);
1292       if (ctx[score] && ctx[score]->isElement())
1293             return toElement(ctx[score])->tick();
1294       if (before[score] && before[score]->isElement()) {
1295             const Element* bef = toElement(before[score]);
1296             Fraction f = bef->tick();
1297             if (bef->isDurationElement()) {
1298                   const DurationElement* de = toDurationElement(bef);
1299                   return f + de->actualTicks();
1300                   }
1301             return f;
1302             }
1303       return Fraction(0,1);
1304       }
1305 
1306 //---------------------------------------------------------
1307 //   describeContext
1308 //---------------------------------------------------------
1309 
describeContext(const ScoreElement * ctx)1310 static QString describeContext(const ScoreElement* ctx)
1311       {
1312       if (!ctx)
1313             return QString("no context");
1314       QString descr;
1315       if (ctx->isElement()) {
1316             const Element* e = toElement(ctx);
1317             if (const Measure* m = toMeasure(e->findMeasure()))
1318                   descr += QString("%1 %2").arg(m->userName()).arg(m->no() + 1);
1319             }
1320       if (!ctx->isMeasure()) {
1321             if (!descr.isEmpty())
1322                   descr += ": ";
1323             descr += ctx->userName();
1324             // TODO: add more info
1325             }
1326       return descr;
1327       }
1328 
1329 //---------------------------------------------------------
1330 //   ContextChange::toString
1331 //    ContextChange elements are not intended to be shown
1332 //    to user, this function is for debugging purposes.
1333 //---------------------------------------------------------
1334 
toString() const1335 QString ContextChange::toString() const
1336       {
1337       return QString("Context change: ctx1 (%1), ctx2(%2)").arg(describeContext(ctx[0]), describeContext(ctx[1]));
1338       }
1339 
1340 //---------------------------------------------------------
1341 //   ElementDiff::sameItem
1342 //---------------------------------------------------------
1343 
sameItem(const BaseDiff & other) const1344 bool ElementDiff::sameItem(const BaseDiff& other) const
1345       {
1346       return BaseDiff::sameItem(other);
1347       }
1348 
1349 //---------------------------------------------------------
1350 //   ElementDiff::afrac
1351 //    Returns position of the changed chunk
1352 //---------------------------------------------------------
1353 
afrac(int score) const1354 Fraction ElementDiff::afrac(int score) const
1355       {
1356       Q_ASSERT(score == 0 || score == 1);
1357       const ScoreElement* se = el[score];
1358       if (se && se->isElement())
1359             return toElement(se)->tick();
1360       return BaseDiff::afrac(score);
1361       }
1362 
1363 //---------------------------------------------------------
1364 //   ElementDiff::toString
1365 //---------------------------------------------------------
1366 
toString() const1367 QString ElementDiff::toString() const
1368       {
1369       QString ctxDescr = describeContext(ctx[0]);
1370       switch(type) {
1371             case DiffType::DELETE:
1372                   return QObject::tr("%1: removed element %2", "scorediff").arg(ctxDescr, el[0]->userName());
1373             case DiffType::INSERT:
1374                   return QObject::tr("%1: inserted element %2", "scorediff").arg(ctxDescr, el[1]->userName());
1375             case DiffType::REPLACE:
1376                   return QObject::tr("%1: replaced element %2 with element %3", "scorediff").arg(ctxDescr, el[0]->userName(), el[1]->userName());
1377             case DiffType::EQUAL:
1378                   Q_ASSERT(el[0]->type() == el[1]->type());
1379                   return QObject::tr("%1: equal element %2", "scorediff").arg(ctxDescr, el[0]->userName());
1380             }
1381       return ctxDescr;
1382       }
1383 
1384 //---------------------------------------------------------
1385 //   PropertyDiff::sameItem
1386 //---------------------------------------------------------
1387 
sameItem(const BaseDiff & otherBase) const1388 bool PropertyDiff::sameItem(const BaseDiff& otherBase) const
1389       {
1390       if (!BaseDiff::sameItem(otherBase))
1391             return false;
1392       if (itemType() != otherBase.itemType())
1393             return false;
1394       const PropertyDiff& other = static_cast<const PropertyDiff&>(otherBase);
1395       return (pid == other.pid);
1396       }
1397 
1398 //---------------------------------------------------------
1399 //   PropertyDiff::toString
1400 //---------------------------------------------------------
1401 
toString() const1402 QString PropertyDiff::toString() const
1403       {
1404       QString ctxDescr = describeContext(ctx[0]);
1405       QString propName = propertyUserName(pid);
1406       switch(propertyType(pid)) {
1407             case P_TYPE::BOOL:
1408                   {
1409                   bool b1 = ctx[0]->getProperty(pid).toBool();
1410                   Q_ASSERT(b1 != ctx[1]->getProperty(pid).toBool());
1411                   QString t;
1412                   if (b1)
1413                         t = QObject::tr("%1: property %2 is turned off", "scorediff");
1414                   else
1415                         t = QObject::tr("%1: property %2 is turned on", "scorediff");
1416                   return t.arg(ctxDescr, propName);
1417                   }
1418             default:
1419                   {
1420                   QString val1 = ctx[0]->propertyUserValue(pid);
1421                   QString val2 = ctx[1]->propertyUserValue(pid);
1422                   QString t = QObject::tr("%1: property %2 changed from %3 to %4", "scorediff");
1423                   return t.arg(ctxDescr, propName, val1, val2);
1424                   }
1425             }
1426       }
1427 
1428 //---------------------------------------------------------
1429 //   MarkupDiff::sameItem
1430 //---------------------------------------------------------
1431 
sameItem(const BaseDiff & otherBase) const1432 bool MarkupDiff::sameItem(const BaseDiff& otherBase) const
1433       {
1434       if (!BaseDiff::sameItem(otherBase))
1435             return false;
1436       if (itemType() != otherBase.itemType())
1437             return false;
1438       const MarkupDiff& other = static_cast<const MarkupDiff&>(otherBase);
1439       return (name == other.name);
1440       }
1441 
1442 //---------------------------------------------------------
1443 //   MarkupDiff::toString
1444 //---------------------------------------------------------
1445 
toString() const1446 QString MarkupDiff::toString() const
1447       {
1448       QString ctxDescr = describeContext(ctx[0]);
1449       if (name == "metaTag") {
1450             QString tagName = info.toString();
1451             return QObject::tr("%1: %2 changed from %3 to %4", "scorediff")
1452                .arg(ctxDescr, tagName,
1453                     ctx[0]->score()->metaTag(tagName), ctx[1]->score()->metaTag(tagName));
1454             }
1455       return QObject::tr("%1: markup changes: %2", "scorediff")
1456          .arg(ctxDescr, name);
1457       }
1458 }
1459 
1460