|
| 1 | +using System.Diagnostics; |
| 2 | +using System.Net; |
| 3 | +using System.Xml.Linq; |
| 4 | + |
| 5 | +namespace OriginLab.DocumentGeneration; |
| 6 | + |
| 7 | +internal class DocToStaticPagesResourceResolver : DocResourceResolver, IDocResourceResolver |
| 8 | +{ |
| 9 | + private readonly DocToStaticPagesTransformerArgs Args; |
| 10 | + |
| 11 | + private string BookUrlName => Args.BookUrlName; |
| 12 | + private bool UseWebp => Args.UseWebp; |
| 13 | + |
| 14 | + private readonly string SourceFolderEn; |
| 15 | + |
| 16 | + public string Language |
| 17 | + { |
| 18 | + get; |
| 19 | + set |
| 20 | + { |
| 21 | + ArgumentException.ThrowIfNullOrWhiteSpace(value); |
| 22 | + |
| 23 | + field = value; |
| 24 | + VisitedImages.Clear(); |
| 25 | + } |
| 26 | + } = null!; |
| 27 | + |
| 28 | + private readonly Dictionary<string, (string book, string url, string titleEn)> PageLinks; |
| 29 | + |
| 30 | + private readonly Dictionary<string, (long size, ulong hash, string url)> EnglishImages = new(StringComparer.OrdinalIgnoreCase); |
| 31 | + |
| 32 | + private readonly Dictionary<string, string> VisitedImages = new(StringComparer.OrdinalIgnoreCase); |
| 33 | + |
| 34 | + public DocToStaticPagesResourceResolver(DocToStaticPagesTransformerArgs args) : base(args) |
| 35 | + { |
| 36 | + SourceFolderEn = Path.Combine(args.SourceFolder, "en"); |
| 37 | + Args = args; |
| 38 | + |
| 39 | + var pages = new List<(string file, string book, string url, string title)>(); |
| 40 | + |
| 41 | + foreach (var xmlFile in Directory.EnumerateFiles(args.BooksXmlFolder, "*.xml")) |
| 42 | + { |
| 43 | + var dirName = Path.GetFileNameWithoutExtension(xmlFile); |
| 44 | + |
| 45 | + foreach (var p in XElement.Load(xmlFile).Descendants("page")) |
| 46 | + { |
| 47 | + var file = $"{dirName}/{p.Attribute("file")!.Value}"; |
| 48 | + var url = p.Attribute("url")!.Value; |
| 49 | + var sep = url.IndexOf('/'); |
| 50 | + var title = p.Attribute("title")!.Value; |
| 51 | + |
| 52 | + pages.Add((file, book: sep < 0 ? url : url[..sep], url: sep < 0 ? "" : url[(sep + 1)..], title)); |
| 53 | + } |
| 54 | + } |
| 55 | + |
| 56 | + PageLinks = pages.ToDictionary(p => p.file, p => (p.book.ToLowerInvariant(), p.url.ToLowerInvariant(), p.title), StringComparer.OrdinalIgnoreCase); |
| 57 | + } |
| 58 | + |
| 59 | + protected override string GetSharedImageSrc(string path, string fileName) |
| 60 | + => $"/books/images/{fileName}?v={FileHash.StringFromFile(path)}"; |
| 61 | + |
| 62 | + public bool TryResolveHref(string href, string sourceDir, out string result, out string? titleEn) |
| 63 | + { |
| 64 | + titleEn = null; |
| 65 | + |
| 66 | + var parts = new UrlParts(href); |
| 67 | + |
| 68 | + if (parts.IsAbosolute || href.StartsWith('/') || href.StartsWith('#')) |
| 69 | + { |
| 70 | + result = href; |
| 71 | + return true; |
| 72 | + } |
| 73 | + |
| 74 | + var path = parts is { Query.Length: 0, Hash.Length: 0 } ? href : parts.Path.ToString(); |
| 75 | + Debug.Assert(!path.IsEmpty); |
| 76 | + |
| 77 | + var fullPath = Path.GetFullPath(path, sourceDir); |
| 78 | + if (fullPath.StartsWith(SourceFolder) |
| 79 | + && Path.GetDirectoryName(Path.GetDirectoryName(Path.GetDirectoryName(fullPath.AsSpan()))) is { IsEmpty: false } targetBookDirContainer) |
| 80 | + { |
| 81 | + var targetFile = WebUtility.UrlDecode(fullPath[(targetBookDirContainer.Length + 1)..].Replace('\\', '/')); |
| 82 | + |
| 83 | + if (PageLinks.TryGetValue(targetFile, out var link) |
| 84 | + || (MovedPages.TryGetValue(targetFile, out var movedToFile) && PageLinks.TryGetValue(movedToFile, out link))) |
| 85 | + { |
| 86 | + if (Language == "en") |
| 87 | + { |
| 88 | + result = '/'.TrySurroundEach(link.book, link.url); |
| 89 | + } |
| 90 | + else |
| 91 | + { |
| 92 | + result = '/'.TrySurroundEach(link.book, link.url, Language); |
| 93 | + } |
| 94 | + |
| 95 | + if (!parts.Query.IsEmpty || !parts.Hash.IsEmpty) |
| 96 | + { |
| 97 | + result = $"{result}{parts.Query}{parts.Hash}"; |
| 98 | + } |
| 99 | + |
| 100 | + titleEn = link.titleEn; |
| 101 | + return true; |
| 102 | + } |
| 103 | + } |
| 104 | + |
| 105 | + result = "Unknown href mapping"; |
| 106 | + return false; |
| 107 | + } |
| 108 | + |
| 109 | + public bool TryResolveSrc(string src, string sourceDir, out string result, out (string src, string dst)? copy) |
| 110 | + { |
| 111 | + var parts = new UrlParts(src); |
| 112 | + |
| 113 | + if (parts.IsAbosolute || src.StartsWith('/')) |
| 114 | + { |
| 115 | + result = src; |
| 116 | + copy = null; |
| 117 | + return true; |
| 118 | + } |
| 119 | + |
| 120 | + var path = parts is { Query.Length: 0, Hash.Length: 0 } ? src : parts.Path.ToString(); |
| 121 | + Debug.Assert(!path.IsEmpty); |
| 122 | + |
| 123 | + var indexOfImages = path.IndexOf("images/"); |
| 124 | + Debug.Assert(indexOfImages > -1); |
| 125 | + |
| 126 | + var srcImg = new FileInfo(Path.GetFullPath(path, sourceDir)); |
| 127 | + var needsCopy = true; |
| 128 | + |
| 129 | + var fileName = Path.GetFileName(path); |
| 130 | + if (SharedImages.TryGetValue(fileName, out result!)) |
| 131 | + { |
| 132 | + needsCopy = false; |
| 133 | + } |
| 134 | + else if (srcImg.Exists) |
| 135 | + { |
| 136 | + result = '/'.TryPrefixEach(BookUrlName, Language, path[indexOfImages..]); |
| 137 | + |
| 138 | + if (Language == "en") |
| 139 | + { |
| 140 | + if (!EnglishImages.TryGetValue(path, out var visited)) |
| 141 | + { |
| 142 | + var size = srcImg.Length; |
| 143 | + var hash = FileHash.UInt64FromFile(srcImg.FullName); |
| 144 | + |
| 145 | + EnglishImages.Add(path, (size, hash, result)); |
| 146 | + } |
| 147 | + else |
| 148 | + { |
| 149 | + result = visited.url; |
| 150 | + needsCopy = false; |
| 151 | + } |
| 152 | + } |
| 153 | + else |
| 154 | + { |
| 155 | + if (VisitedImages.TryGetValue(path, out var prevUrl)) |
| 156 | + { |
| 157 | + result = prevUrl; |
| 158 | + needsCopy = false; |
| 159 | + } |
| 160 | + else |
| 161 | + { |
| 162 | + VisitedImages.Add(path, result); |
| 163 | + |
| 164 | + if (EnglishImages.TryGetValue(path, out var visited) && srcImg.Length == visited.size && FileHash.UInt64FromFile(srcImg.FullName) == visited.hash) |
| 165 | + { |
| 166 | + result = VisitedImages[path] = visited.url; |
| 167 | + needsCopy = false; |
| 168 | + } |
| 169 | + } |
| 170 | + } |
| 171 | + } |
| 172 | + else |
| 173 | + { |
| 174 | + var srcImgEn = $"{SourceFolderEn}{srcImg.FullName.AsSpan(SourceFolderEn.Length)}"; |
| 175 | + |
| 176 | + if (!File.Exists(srcImgEn)) |
| 177 | + { |
| 178 | + result = "Image src not found"; |
| 179 | + copy = null; |
| 180 | + return false; |
| 181 | + } |
| 182 | + |
| 183 | + result = '/'.TryPrefixEach(BookUrlName, "en", path[indexOfImages..]); |
| 184 | + needsCopy = false; |
| 185 | + } |
| 186 | + |
| 187 | + if (UseWebp) |
| 188 | + { |
| 189 | + var resultDir = Path.GetDirectoryName(result.AsSpan()); |
| 190 | + var resultFileName = Path.GetFileNameWithoutExtension(result.AsSpan()); |
| 191 | + |
| 192 | + result = $"{resultDir}/{resultFileName}.webp"; |
| 193 | + } |
| 194 | + |
| 195 | + if (!needsCopy) |
| 196 | + { |
| 197 | + copy = null; |
| 198 | + } |
| 199 | + else |
| 200 | + { |
| 201 | + var dstImg = Path.Combine(OutputFolder, Language, path[indexOfImages..]); |
| 202 | + copy = (srcImg.FullName, dstImg); |
| 203 | + } |
| 204 | + |
| 205 | + if (!parts.Query.IsEmpty) |
| 206 | + { |
| 207 | + result = $"{result}{parts.Query}"; |
| 208 | + } |
| 209 | + |
| 210 | + return true; |
| 211 | + } |
| 212 | +} |
0 commit comments