// 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(@"
Files
"), sectionCommits = XElement.Parse(@"
Changes
"), 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); } } }