using System.Text.Json;
using AngleSharp.Dom;
using Cosmos.Kernel.System.Graphics.Fonts;
using XFESystem.Browser;
using XFESystem.Documents;
using XFESystem.Kernel;
System.Runtime.Loader.AssemblyLoadContext.Default.Resolving+=(_,name)=>{string path=Path.Combine(AppContext.BaseDirectory,name.Name+".dll");return File.Exists(path)?System.Runtime.Loader.AssemblyLoadContext.Default.LoadFromAssemblyPath(path):null;};
Probe.Run(args);
static class Probe
{
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
public static void Run(string[] args)
{
string root=Path.GetFullPath(args[0]),reference=Path.GetFullPath(args[1]);int width=args.Length>2?int.Parse(args[2]):796,height=args.Length>3?int.Parse(args[3]):404;
string output=Path.Combine(root,"build/CSharpDocuments/WebsiteLayout-"+DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());Directory.CreateDirectory(output);
var metrics=new Metrics(root);var entries=new List<object>();
using var original=JsonDocument.Parse(File.ReadAllText(reference));
foreach(bool dark in new[]{false,true})
{
var browser=new BrowserService(new RejectNetwork());browser.ResizeViewport(width,height);browser.NavigateDocument(WebsiteAssets.ReadText("Fixtures/WebsiteParity.md"),"WebsiteParity.md",dark);
browser.Layout(new(width,height),metrics);
for(int frame=1;frame<=8;frame++)browser.AdvanceTime(frame*20);
var layout=browser.Layout(new(width,height),metrics);var doc=browser.Document!;string theme=dark?"dark":"light";
var expected=original.RootElement.GetProperty("States").EnumerateArray().Single(state=>state.GetProperty("Theme").GetString()==theme);
var comparison=new List<object>();
foreach(var item in new[]{("Article",".markdown-body"),("Heading","h1"),("Code","pre.code-block-enhanced"),("Quote","blockquote"),("Table","table")})
{
var node=doc.QuerySelector(item.Item2)??throw new InvalidOperationException("Missing selector "+item.Item2+"; "+browser.RefreshDocument().Text);var box=layout.GetBoundingClientRect(node);var style=layout.GetComputedStyle(node)!;var e=expected.GetProperty(item.Item1);
comparison.Add(new{Name=item.Item1,Actual=box,Reference=e,Delta=new{X=box.X-e.GetProperty("X").GetDouble(),Y=box.Y-e.GetProperty("Y").GetDouble(),Width=box.Width-e.GetProperty("Width").GetDouble(),Height=box.Height-e.GetProperty("Height").GetDouble()},Font=style.Font,style.LineHeight,Background=style.Background,Properties=style.Properties});
}
File.WriteAllText(Path.Combine(output,(dark?"Dark":"Light")+"Dom.html"),doc.DocumentElement!.OuterHtml);
File.WriteAllText(Path.Combine(output,(dark?"Dark":"Light")+"Readback.json"),browser.ReadWebsiteDocumentProof());
var boxes=layout.Boxes.Where(box=>box.Node is IElement).Select(box=>new{Tag=((IElement)box.Node).LocalName,Class=((IElement)box.Node).ClassName,box.ContentBox,box.BorderBox,box.MarginBox,box.PaddingBox,box.Style.Font,box.Style.LineHeight}).ToArray();
File.WriteAllText(Path.Combine(output,(dark?"Dark":"Light")+"Boxes.json"),JsonSerializer.Serialize(boxes,new JsonSerializerOptions{WriteIndented=true}));
entries.Add(new{Theme=theme,Comparison=comparison,layout.DocumentHeight,Diagnostics=layout.Diagnostics,GuestTested=false});
}
string result=Path.Combine(output,"Result.json");File.WriteAllText(result,JsonSerializer.Serialize(new{Schema="XfeWebsiteLayoutProbe1",HostOnly=true,NativeGuestTested=false,HostBrowserUsed=false,Viewport=new{Width=width,Height=height},ReferencePath=reference,FontFallback="Real pinned Cosmos TrueType advance/kerning: Lato Regular/Bold, Cascadia Mono, XFE Web CJK Regular/Bold when primary glyph is absent; final missing glyph uses existing fallback advance",States=entries},new JsonSerializerOptions{WriteIndented=true}));Console.WriteLine(result);
}
private sealed class RejectNetwork:IWebTransport{public WebResponse Get(Uri address,BrowserLimits limits)=>throw new InvalidOperationException("Layout probe cannot access network: "+address);}
private sealed class Metrics:IBrowserTextMetrics
{
private readonly (TrueTypeFont Font,NativeWebFontMetrics Metrics) mono,sans,bold,cjk,cjkBold;
public Metrics(string root){(TrueTypeFont,NativeWebFontMetrics) Load(string relative){byte[] bytes=File.ReadAllBytes(Path.Combine(root,relative));return(new(bytes),new(bytes));}mono=Load("assets/fonts/cascadia-mono-2407.24/CascadiaMono-Regular.ttf");sans=Load("assets/fonts/lato-2.015/Lato-Regular.ttf");bold=Load("assets/fonts/lato-2.015/Lato-Bold.ttf");cjk=Load("assets/fonts/noto-sans-sc-2.004/XfeWebCjk-Regular.ttf");cjkBold=Load("assets/fonts/noto-sans-sc-2.004/XfeWebCjk-Bold.ttf");}
public double Measure(string text,BrowserFont style)
{
var face=style.Family.Contains("monospace",StringComparison.OrdinalIgnoreCase)||style.Family.Contains("Consolas",StringComparison.OrdinalIgnoreCase)?mono:style.Weight>=600?bold:sans;
int height=(int)Math.Round(style.Size*64*face.Metrics.LineToEm);double value=0;char previous='\0';
foreach(char ch in text){if(previous!='\0'&&face.Font.HasGlyph(previous)&&face.Font.HasGlyph(ch))value+=face.Font.GetKerning(previous,ch,height)/64d;var selected=face.Font.HasGlyph(ch)?face:style.Weight>=600?cjkBold:cjk;int selectedHeight=(int)Math.Round(style.Size*64*selected.Metrics.LineToEm);value+=selected.Font.HasGlyph(ch)?selected.Font.GetAdvance(ch,selectedHeight)/64d:style.Size;previous=ch;}return value;
}
}
}
using System.Text.Json;
using AngleSharp.Dom;
using Cosmos.Kernel.System.Graphics.Fonts;
using XFESystem.Browser;
using XFESystem.Documents;
using XFESystem.Kernel;
System.Runtime.Loader.AssemblyLoadContext.Default.Resolving+=(_,name)=>{string path=Path.Combine(AppContext.BaseDirectory,name.Name+".dll");return File.Exists(path)?System.Runtime.Loader.AssemblyLoadContext.Default.LoadFromAssemblyPath(path):null;};
Probe.Run(args);
static class Probe
{
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
public static void Run(string[] args)
{
string root=Path.GetFullPath(args[0]),reference=Path.GetFullPath(args[1]);int width=args.Length>2?int.Parse(args[2]):796,height=args.Length>3?int.Parse(args[3]):404;
string output=Path.Combine(root,"build/CSharpDocuments/WebsiteLayout-"+DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());Directory.CreateDirectory(output);
var metrics=new Metrics(root);var entries=new List<object>();
using var original=JsonDocument.Parse(File.ReadAllText(reference));
foreach(bool dark in new[]{false,true})
{
var browser=new BrowserService(new RejectNetwork());browser.ResizeViewport(width,height);browser.NavigateDocument(WebsiteAssets.ReadText("Fixtures/WebsiteParity.md"),"WebsiteParity.md",dark);
browser.Layout(new(width,height),metrics);
for(int frame=1;frame<=8;frame++)browser.AdvanceTime(frame*20);
var layout=browser.Layout(new(width,height),metrics);var doc=browser.Document!;string theme=dark?"dark":"light";
var expected=original.RootElement.GetProperty("States").EnumerateArray().Single(state=>state.GetProperty("Theme").GetString()==theme);
var comparison=new List<object>();
foreach(var item in new[]{("Article",".markdown-body"),("Heading","h1"),("Code","pre.code-block-enhanced"),("Quote","blockquote"),("Table","table")})
{
var node=doc.QuerySelector(item.Item2)??throw new InvalidOperationException("Missing selector "+item.Item2+"; "+browser.RefreshDocument().Text);var box=layout.GetBoundingClientRect(node);var style=layout.GetComputedStyle(node)!;var e=expected.GetProperty(item.Item1);
comparison.Add(new{Name=item.Item1,Actual=box,Reference=e,Delta=new{X=box.X-e.GetProperty("X").GetDouble(),Y=box.Y-e.GetProperty("Y").GetDouble(),Width=box.Width-e.GetProperty("Width").GetDouble(),Height=box.Height-e.GetProperty("Height").GetDouble()},Font=style.Font,style.LineHeight,Background=style.Background,Properties=style.Properties});
}
File.WriteAllText(Path.Combine(output,(dark?"Dark":"Light")+"Dom.html"),doc.DocumentElement!.OuterHtml);
File.WriteAllText(Path.Combine(output,(dark?"Dark":"Light")+"Readback.json"),browser.ReadWebsiteDocumentProof());
var boxes=layout.Boxes.Where(box=>box.Node is IElement).Select(box=>new{Tag=((IElement)box.Node).LocalName,Class=((IElement)box.Node).ClassName,box.ContentBox,box.BorderBox,box.MarginBox,box.PaddingBox,box.Style.Font,box.Style.LineHeight}).ToArray();
File.WriteAllText(Path.Combine(output,(dark?"Dark":"Light")+"Boxes.json"),JsonSerializer.Serialize(boxes,new JsonSerializerOptions{WriteIndented=true}));
entries.Add(new{Theme=theme,Comparison=comparison,layout.DocumentHeight,Diagnostics=layout.Diagnostics,GuestTested=false});
}
string result=Path.Combine(output,"Result.json");File.WriteAllText(result,JsonSerializer.Serialize(new{Schema="XfeWebsiteLayoutProbe1",HostOnly=true,NativeGuestTested=false,HostBrowserUsed=false,Viewport=new{Width=width,Height=height},ReferencePath=reference,FontFallback="Real pinned Cosmos TrueType advance/kerning: Lato Regular/Bold, Cascadia Mono, XFE Web CJK Regular/Bold when primary glyph is absent; final missing glyph uses existing fallback advance",States=entries},new JsonSerializerOptions{WriteIndented=true}));Console.WriteLine(result);
}
private sealed class RejectNetwork:IWebTransport{public WebResponse Get(Uri address,BrowserLimits limits)=>throw new InvalidOperationException("Layout probe cannot access network: "+address);}
private sealed class Metrics:IBrowserTextMetrics
{
private readonly (TrueTypeFont Font,NativeWebFontMetrics Metrics) mono,sans,bold,cjk,cjkBold;
public Metrics(string root){(TrueTypeFont,NativeWebFontMetrics) Load(string relative){byte[] bytes=File.ReadAllBytes(Path.Combine(root,relative));return(new(bytes),new(bytes));}mono=Load("assets/fonts/cascadia-mono-2407.24/CascadiaMono-Regular.ttf");sans=Load("assets/fonts/lato-2.015/Lato-Regular.ttf");bold=Load("assets/fonts/lato-2.015/Lato-Bold.ttf");cjk=Load("assets/fonts/noto-sans-sc-2.004/XfeWebCjk-Regular.ttf");cjkBold=Load("assets/fonts/noto-sans-sc-2.004/XfeWebCjk-Bold.ttf");}
public double Measure(string text,BrowserFont style)
{
var face=style.Family.Contains("monospace",StringComparison.OrdinalIgnoreCase)||style.Family.Contains("Consolas",StringComparison.OrdinalIgnoreCase)?mono:style.Weight>=600?bold:sans;
int height=(int)Math.Round(style.Size*64*face.Metrics.LineToEm);double value=0;char previous='\0';
foreach(char ch in text){if(previous!='\0'&&face.Font.HasGlyph(previous)&&face.Font.HasGlyph(ch))value+=face.Font.GetKerning(previous,ch,height)/64d;var selected=face.Font.HasGlyph(ch)?face:style.Weight>=600?cjkBold:cjk;int selectedHeight=(int)Math.Round(style.Size*64*selected.Metrics.LineToEm);value+=selected.Font.HasGlyph(ch)?selected.Font.GetAdvance(ch,selectedHeight)/64d:style.Size;previous=ch;}return value;
}
}
}