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(); 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(); 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; } } }