Skip to content

Commit 5ec6659

Browse files
committed
Decouple resolving and transforming.
1 parent 227f706 commit 5ec6659

10 files changed

Lines changed: 296 additions & 258 deletions

converter/generator/DocBookTransformer.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ internal sealed class DocBookTransformer : DocToStaticPagesTransformer
1212
private readonly string BookDirName;
1313
private readonly (string url, string file, string titleEn, NavFiles navFiles)[] Pages;
1414

15-
public DocBookTransformer(DocToStaticPagesTransformerArgs args, ProblemRecorder problems) : base(args, problems)
15+
public DocBookTransformer(DocToStaticPagesTransformerArgs args, IDocResourceResolver resourceResolver, ProblemRecorder problems) : base(args, resourceResolver, problems)
1616
{
1717
AvailableLanguagesExpression = String.Join(',', AvailableLanguages);
1818
BookDirName = Path.GetFileName(Directory.EnumerateDirectories(Path.Combine(SourceFolder, "en")).Single());
@@ -151,7 +151,7 @@ private IHtmlDivElement CreateNavDataDiv(IHtmlDocument document, in Nav nav, str
151151

152152
if (!files.Parent.IsEmpty)
153153
{
154-
if (TryResolveHref("../" + files.Parent, sourceDir, out var url, out var _))
154+
if (ResourceResolver.TryResolveHref("../" + files.Parent, sourceDir, out var url, out var _))
155155
{
156156
navDataDiv.SetAttribute("data-parent-link", url);
157157
}

converter/generator/DocIndexTransformer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ namespace OriginLab.DocumentGeneration;
44

55
internal class DocIndexTransformer : DocToStaticPagesTransformer
66
{
7-
public DocIndexTransformer(DocToStaticPagesTransformerArgs args, ProblemRecorder problems) : base(args, problems)
7+
public DocIndexTransformer(DocToStaticPagesTransformerArgs args, IDocResourceResolver resourceResolver, ProblemRecorder problems) : base(args, resourceResolver, problems)
88
{
99
}
1010

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using System.Text.Json;
2+
using OriginLab.DocumentGeneration.Templates;
3+
4+
namespace OriginLab.DocumentGeneration;
5+
6+
internal abstract class DocResourceResolver
7+
{
8+
private readonly DocTransformerArgs Args;
9+
protected string SourceFolder => Args.SourceFolder;
10+
protected string OutputFolder => Args.OutputFolder;
11+
protected string BooksXmlFolder => Args.BooksXmlFolder;
12+
13+
protected Dictionary<string, string> SharedImages => field ??= GetSharedImages();
14+
15+
protected Dictionary<string, string> MovedPages => field ??= GetMovedPages();
16+
17+
public DocResourceResolver(DocTransformerArgs args)
18+
{
19+
Args = args;
20+
}
21+
22+
protected abstract string GetSharedImageSrc(string path, string fileName);
23+
24+
private Dictionary<string, string> GetSharedImages()
25+
{
26+
var images = new Dictionary<string, string>();
27+
28+
foreach (var path in Directory.EnumerateFiles(Path.Combine(Template.WebRootPath, "books/images")))
29+
{
30+
var fileName = Path.GetFileName(path);
31+
images.Add(fileName, GetSharedImageSrc(path, fileName));
32+
}
33+
34+
return images;
35+
}
36+
37+
private Dictionary<string, string> GetMovedPages()
38+
{
39+
using var movedJson = File.OpenRead(Path.Combine(BooksXmlFolder, "Moved.json"));
40+
#pragma warning disable CA1869 // Cache and reuse 'JsonSerializerOptions' instances
41+
return JsonSerializer.Deserialize<Dictionary<string, string>>(movedJson, new JsonSerializerOptions
42+
#pragma warning restore CA1869 // Cache and reuse 'JsonSerializerOptions' instances
43+
{
44+
AllowTrailingCommas = true,
45+
ReadCommentHandling = JsonCommentHandling.Skip,
46+
PropertyNameCaseInsensitive = true,
47+
AllowDuplicateProperties = true,
48+
})
49+
?.ToDictionary(StringComparer.OrdinalIgnoreCase) ?? [];
50+
}
51+
}
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
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

Comments
 (0)