using System.Security.Cryptography; using System.Text.Json; using System.Text.RegularExpressions; using AngleSharp.Dom; using AngleSharp.Html.Parser; using XFESystem.Browser; using XFESystem.Documents; if(args.Length!=3)throw new ArgumentException("repository root, website root and output file are required"); string root=Path.GetFullPath(args[0]),website=Path.GetFullPath(args[1]),output=Path.GetFullPath(args[2]); var parser=new HtmlParser(); // Use the real generated article shell and its exact stylesheet order. No JS, // Monaco placeholder, host browser or hand-written selector precedence is used. var document=parser.ParseDocument(new DocumentPreviewBuilder(new()).MarkdownWeb("",dark:true).Html); var manifest=JsonDocument.Parse(WebsiteAssets.ReadText("Exact/SourceManifest.json")); var cssSources=new List();var sheets=new List(); var selectors=new HashSet(StringComparer.Ordinal); foreach(var link in document.QuerySelectorAll("link[rel='stylesheet']")) { string url=link.GetAttribute("href")!; var resource=WebsiteAssets.WebResources.Single(entry=>entry.UrlPath==url); byte[] embedded;using(var stream=WebsiteAssets.Open(resource.AssetName)){using var bytes=new MemoryStream();stream.CopyTo(bytes);embedded=bytes.ToArray();} var entry=manifest.RootElement.GetProperty("Entries").EnumerateArray().Single(value=>value.TryGetProperty("UrlPath",out var path)&&path.GetString()==url); string sourcePath=entry.GetProperty("SourcePath").GetString()!; byte[] original=File.ReadAllBytes(Path.Combine(website,sourcePath)); string sha=Convert.ToHexStringLower(SHA256.HashData(embedded)); if(!embedded.AsSpan().SequenceEqual(original)||sha!=entry.GetProperty("SourceSha256").GetString())throw new InvalidOperationException("Website stylesheet bytes differ: "+url); cssSources.Add(new{UrlPath=url,SourcePath=sourcePath,SnapshotPath="Source/XFESystem.Documents/Assets/Website/"+resource.AssetName,Sha256=sha,Bytes=embedded.Length}); string css=System.Text.Encoding.UTF8.GetString(embedded); sheets.Add(new(css,link)); foreach(Match rule in Regex.Matches(Regex.Replace(css,@"/\*.*?\*/","",RegexOptions.Singleline),@"([^{}]+)\{([^{}]*)\}")) foreach(string candidate in rule.Groups[1].Value.Split(',')) { string selector=candidate.Trim(); if(selector!=".hljs"&&selector.Contains(".hljs-",StringComparison.Ordinal)&&Regex.IsMatch(selector,@"^(?:\.[\w-]+)+(?:\s+(?:\.[\w-]+)+)*$"))selectors.Add(selector); } } var article=document.QuerySelector(".markdown-body")!; article.InnerHtml="
plain
"; var code=article.QuerySelector("code")!; var samples=new List<(string Selector,string[] Scopes,IElement Leaf)>{("",[],document.GetElementById("PalettePlain")!)}; foreach(string selector in selectors.Order(StringComparer.Ordinal)) { IElement parent=code;var scopes=new List(); foreach(string segment in Regex.Split(selector,@"\s+")) { var classes=Regex.Matches(segment,@"\.([\w-]+)").Select(match=>match.Groups[1].Value).ToArray();scopes.AddRange(classes); var span=document.CreateElement("span");span.ClassName=string.Join(' ',classes);parent.AppendChild(span);parent=span; } parent.TextContent="X";samples.Add((selector,scopes.ToArray(),parent));code.AppendChild(document.CreateTextNode(" ")); } var layout=new BrowserLayoutEngine().Layout(document,new(800,600),sheets); var rules=samples.Select(sample=>{ var computed=layout.GetComputedStyle(sample.Leaf)??throw new InvalidOperationException("No computed token style: "+sample.Selector); string token=sample.Scopes.Length==0?"xfe":"xfe."+string.Join('.',sample.Scopes.Select(scope=>scope.StartsWith("hljs-",StringComparison.Ordinal)?scope[5..].Replace('-','_'):scope.Replace('-','_'))); string fontStyle=(computed.Font.Italic?"italic ":"")+(computed.Font.Weight>=600?"bold":""); return new{Selector=sample.Selector,Scopes=sample.Scopes,Token=token,Foreground=computed.Color.Rgb.ToString("x6"), Background=computed.Background.Alpha>=1?computed.Background.Rgb.ToString("x6"):null, FontStyle=fontStyle.Trim(),ComputedFontFamily=computed.Font.Family}; }).ToArray(); string cascadeSha=Convert.ToHexStringLower(SHA256.HashData(JsonSerializer.SerializeToUtf8Bytes(cssSources))); var result=new{Kind="WebsiteActualArticleCascadePalette",CascadeCssSha256=cascadeSha,CssSources=cssSources, CssSha256=cssSources.Select(value=>JsonSerializer.SerializeToElement(value)).Single(value=>value.GetProperty("UrlPath").GetString()=="/css/docs/code-style.css").GetProperty("Sha256").GetString(), Derivation="Real generated MarkdownWeb shell + all original styles in original order; production CSS specificity, inheritance and !important resolver; no last-rule grep",Rules=rules}; Directory.CreateDirectory(Path.GetDirectoryName(output)!);File.WriteAllText(output,JsonSerializer.Serialize(result,new JsonSerializerOptions{WriteIndented=true})); Console.WriteLine(output);