// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows.Documents;
using System.Windows.Markup;
using System.Xml.Linq;
using DiffPlex.DiffBuilder;
using DiffPlex.DiffBuilder.Model;
using Newtonsoft.Json.Linq;
using JsonFormatting = Newtonsoft.Json.Formatting;
namespace QtVsTools.Core.MsBuild
{
using Common;
using QtVsTools.Common;
using static Common.Utils;
using static MsBuildProjectReaderWriter;
public class ConversionReport
{
public static ConversionReport Generate(ConversionData data)
{
if (GenerateReportXaml(data) is not { Length: > 0 } xaml)
return null;
return new ConversionReport { Xaml = xaml };
}
public bool Save(string path)
{
try {
var xamlClean = Xaml.TrimEnd(' ', '\r', '\n') + "\r\n";
var xamlUtf8 = Encoding.UTF8.GetBytes(xamlClean);
using var xamlStream = File.Open(path, FileMode.Create);
using var xamlFile = new BinaryWriter(xamlStream);
xamlFile.Write(xamlUtf8);
xamlFile.Write(Encoding.UTF8.GetBytes
($"\r\n"));
xamlFile.Flush();
xamlFile.Close();
} catch (Exception e) {
e.Log();
return false;
}
MsBuildProject.ShowProjectFormatUpdated(path);
return true;
}
public static ConversionReport Load(string path)
{
var report = new ConversionReport();
try {
var xamlUtf8 = File.ReadAllBytes(path);
var footerIdx = xamlUtf8.LastIndexOfArray(Encoding.UTF8.GetBytes("");
if (!cksum.Success)
return null;
var xamlHash = Convert.FromBase64String(cksum.Groups["hash"].Value);
if (!xamlHash.SequenceEqual(Hash(xamlUtf8, 0, footerIdx)))
return null;
report.Xaml = Encoding.UTF8.GetString(xamlUtf8, 0, footerIdx);
} catch (Exception e) {
e.Log();
return null;
}
return report;
}
private ConversionReport()
{ }
private LazyFactory Lazy { get; } = new();
private string Xaml { get; set; }
public FlowDocument Document => Lazy.Get(() => Document, () =>
{
try {
if (XamlReader.Parse(Xaml) is FlowDocument doc)
return doc;
} catch (Exception) {
return null;
}
return null;
});
private const string DefaultFont = "Segoe UI";
private const string DefaultMonospacedFont = "Consolas";
private const double DefaultFontSize = 12.0;
private static string GenerateReportXaml(ConversionData data)
{
XElement sectionFiles;
XElement sectionCommits;
var doc = new XDocument(
new XElement("FlowDocument",
new XAttribute("FontFamily", DefaultFont),
new XAttribute("FontSize", DefaultFontSize),
XElement.Parse($@"
"),
XElement.Parse($@"
Qt Visual Studio Tools
Project Format Conversion
Report generated on {data.DateTime:yyyy-MM-dd HH:mm:ss}
"),
sectionFiles = XElement.Parse(@"
"),
sectionCommits = XElement.Parse(@"
"),
XElement.Parse(@"
")));
foreach (var file in data.FilesChanged)
GenerateFileReport(sectionFiles, file);
foreach (var commit in data.Commits)
GenerateCommitReport(sectionCommits, commit);
return Regex.Replace(doc.ToString(), @"(\r?\n)+\s*
[Before]
[After]
[Diff before/after]
[Diff before/current]
[Diff after/current]
"));
}
private static void GenerateCommitReport(XElement sectionCommits, CommitData commit)
{
sectionCommits.Add(XElement.Parse($@"
"));
foreach (var file in commit.Changes) {
XElement diffTable;
sectionCommits.Add(
diffTable = XElement.Parse($@"
"));
GenerateDiffReport(diffTable,
SideBySideDiffBuilder.Instance.BuildDiffModel(file.Before, file.After));
}
}
private static void GenerateDiffReport(XElement table, SideBySideDiffModel diff)
{
var oldLines = diff.OldText.Lines;
var newLines = diff.NewText.Lines;
Debug.Assert(oldLines.Count == newLines.Count);
var rows = table.Element("TableRowGroup");
Debug.Assert(rows is not null);
int lastLine = -1;
for (int i = 0; i < Math.Min(oldLines.Count, newLines.Count); ++i) {
var left = oldLines[i];
var right = newLines[i];
if ((left.Type == ChangeType.Imaginary || left.Type == ChangeType.Unchanged)
&& right.Type == left.Type) {
continue;
}
if (lastLine == -1 || lastLine < i - 1) {
rows.Add(XElement.Parse(@"
"));
}
lastLine = i;
XElement row;
rows.Add(row = new XElement("TableRow"));
GenerateDiffLineReport(row, left);
GenerateDiffLineReport(row, right);
}
}
private static void GenerateDiffLineReport(XElement row, DiffPiece line)
{
XElement cell;
row.Add(new XElement("TableCell",
new XAttribute("BorderThickness", "0, 0, 0.5, 0"),
new XAttribute("BorderBrush", "Gray"),
cell = XElement.Parse($@"
")));
if (line.Type == ChangeType.Imaginary)
return;
var linePieces = line.Type switch
{
ChangeType.Modified => line.SubPieces,
_ => new() { line }
};
var span = new StringBuilder();
for (int j = 0; j < linePieces.Count; ++j) {
var piece = linePieces[j];
if (piece.Type == ChangeType.Imaginary)
continue;
span.Append(piece.Text);
if (linePieces.ElementAtOrDefault(j + 1) is { } next
&& next.Type == piece.Type) {
continue;
}
span.Replace(' ', '\x00a0');
switch (piece.Type) {
case ChangeType.Unchanged:
cell.Add(XElement.Parse($@"
"));
break;
case ChangeType.Modified:
cell.Add(XElement.Parse($@"
"));
break;
case ChangeType.Deleted:
cell.Add(XElement.Parse($@"
"));
break;
case ChangeType.Inserted:
cell.Add(XElement.Parse($@"
"));
break;
}
span.Clear();
}
}
private static string EmbeddedMetadata(ConversionData data)
{
var metadata = new JObject
{
["timestamp"] = $"{data.DateTime:yyyy-MM-dd HH:mm:ss}",
["files"] = new JObject()
};
foreach (var file in data.FilesChanged) {
metadata["files"][file.Path] = new JObject
{
["before"] = file.Before.ToZipBase64(),
["after"] = file.After.ToZipBase64()
};
}
return metadata.ToString(JsonFormatting.Indented);
}
}
}