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<object>();var sheets=new List<BrowserStyleSheet>();
var selectors=new HashSet<string>(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="<pre class='code-block-enhanced'><span class='code-block-viewport'><code class='language-csharp hljs'><span id='PalettePlain'>plain</span></code></span></pre>";
var code=article.QuerySelector("code")!;
var samples=new List<(string Selector,string[] Scopes,IElement Leaf)>{("<plain code text>",[],document.GetElementById("PalettePlain")!)};
foreach(string selector in selectors.Order(StringComparer.Ordinal))
{
IElement parent=code;var scopes=new List<string>();
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);
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<object>();var sheets=new List<BrowserStyleSheet>();
var selectors=new HashSet<string>(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="<pre class='code-block-enhanced'><span class='code-block-viewport'><code class='language-csharp hljs'><span id='PalettePlain'>plain</span></code></span></pre>";
var code=article.QuerySelector("code")!;
var samples=new List<(string Selector,string[] Scopes,IElement Leaf)>{("<plain code text>",[],document.GetElementById("PalettePlain")!)};
foreach(string selector in selectors.Order(StringComparer.Ordinal))
{
IElement parent=code;var scopes=new List<string>();
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);