XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFESystem

【Rust】XFE系统,基于Rust和汇编

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Net;
using System.Security.Cryptography;
using System.Text;

namespace XFESystem.Documents;

public sealed partial class DocumentPreviewBuilder(MarkdownRenderer markdownRenderer)
{
    public PreviewDocument Markdown(string text, string title = "Markdown", string language = "zh-CN") =>
        Build(markdownRenderer.RenderHtml(text), title, language);

    public PreviewDocument Code(string text, string path, string language = "zh-CN") =>
        Build($"<pre data-code-block-tools><code class=\"language-{CodeHighlightLanguage.FromPath(path)}\">{E(text)}</code></pre>",
            path, language);

    private static PreviewDocument Build(string body, string title, string language)
    {
        var nonce = Convert.ToBase64String(RandomNumberGenerator.GetBytes(24));
        const string resourceOrigin = "https://xfe-resources.invalid";
        var policy = $"default-src 'none'; script-src 'nonce-{nonce}' {resourceOrigin}; style-src {resourceOrigin} 'unsafe-inline'; " +
            $"font-src {resourceOrigin}; img-src data: blob:; connect-src 'none'; object-src 'none'; frame-src 'none'; base-uri 'none'; form-action 'none'";
        var styles = string.Join("\n", WebsiteAssets.StyleNames.Select(name =>
            $"<link rel=\"stylesheet\" href=\"{WebsiteAssets.BaseUri}{name}\">"));
        var scripts = string.Join("\n", WebsiteAssets.ScriptNames.Select(name =>
            $"<script nonce=\"{nonce}\" src=\"{WebsiteAssets.BaseUri}{name}\"></script>"));
        // The website's auto initializer is disabled because its image modal depends on Bootstrap.
        // Keep its actual code tools, math and Mermaid functions, using local resources only.
        var html = $$$"""
            <!doctype html><html lang="{{{E(language)}}}" class="dark"><head><meta charset="utf-8">
            <meta name="viewport" content="width=device-width,initial-scale=1">
            <meta http-equiv="Content-Security-Policy" content="{{{E(policy)}}}"><title>{{{E(title)}}}</title>
            {{{styles}}}
            <style>html{background:#101729;color:#deedff}body{margin:0;padding:24px;font-family:system-ui,sans-serif}.markdown-body{max-width:100%;overflow-wrap:anywhere}.code-tool-button::before,.mermaid-tool-button::before{content:attr(data-tool-label);font:12px system-ui}.code-tool-button,.markdown-body .mermaid-tool-button{width:auto;max-width:none;min-width:32px;padding:0 8px;white-space:nowrap;flex:0 0 auto}.markdown-body .mermaid-toolbar__actions{min-width:0;flex:0 1 auto}.markdown-body .mermaid-toolbar{flex-wrap:wrap}pre{overflow:auto}@media(max-width:480px){body{padding:12px}.markdown-body .mermaid-toolbar{align-items:flex-start}}</style>
            </head><body data-manual-doc-init="true"><main class="markdown-body" data-no-localize>{{{body}}}</main>
            {{{scripts}}}
            <script nonce="{{{nonce}}}">DocUtils.applyCodeFeatures(document.querySelector('main'));DocUtils.initMathRendering(document.querySelector('main'));DocUtils.initMermaid(document.querySelector('main'));(()=>{const zh=document.documentElement.lang.startsWith('zh');const labels=zh?{lines:'行号',download:'下载',expand:'展开',copy:'复制','zoom-out':'缩小','zoom-in':'放大',fit:'适应','download-svg':'SVG','download-png':'PNG'}:{lines:'Lines',download:'Save',expand:'Expand',copy:'Copy','zoom-out':'−','zoom-in':'+',fit:'Fit','download-svg':'SVG','download-png':'PNG'};document.querySelectorAll('[data-code-action],[data-mermaid-action]').forEach(b=>{const action=b.dataset.codeAction||b.dataset.mermaidAction;b.dataset.toolLabel=labels[action]||action;});})();</script>
            </body></html>
            """;
        return new PreviewDocument(html, true, policy);
    }

    public static JavaScriptPreviewPlan JavaScript(string source, TimeSpan? timeout = null, long memoryLimitBytes = 64 * 1024 * 1024)
    {
        ArgumentNullException.ThrowIfNull(source);
        var actualTimeout = timeout ?? TimeSpan.FromSeconds(2);
        if (actualTimeout <= TimeSpan.Zero || actualTimeout > TimeSpan.FromSeconds(30)) throw new ArgumentOutOfRangeException(nameof(timeout));
        if (memoryLimitBytes < 4 * 1024 * 1024 || memoryLimitBytes > 512 * 1024 * 1024) throw new ArgumentOutOfRangeException(nameof(memoryLimitBytes));
        if (Encoding.UTF8.GetByteCount(source) > 1024 * 1024) throw new ArgumentException("JavaScript source exceeds the preview limit.", nameof(source));
        var nonce = Convert.ToBase64String(RandomNumberGenerator.GetBytes(24));
        var code = Convert.ToBase64String(Encoding.UTF8.GetBytes(source));
        // Base64 data URL prevents </script> in user code from escaping into HTML. No eval or host bridge.
        var html = $$"""
            <!doctype html><html><head><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'nonce-{{nonce}}'; style-src 'unsafe-inline'; connect-src 'none'; frame-src 'none'; worker-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'"></head>
            <body><pre id="Output"></pre><script nonce="{{nonce}}">(()=>{const o=document.getElementById('Output');const write=(...a)=>{if(o.textContent.length<65536)o.textContent+=a.map(String).join(' ')+'\n';};for(const n of ['log','info','warn','error'])console[n]=write;addEventListener('error',e=>write(e.message));addEventListener('unhandledrejection',e=>write(String(e.reason)));})();</script>
            <script nonce="{{nonce}}" src="data:text/javascript;base64,{{code}}"></script></body></html>
            """;
        return new JavaScriptPreviewPlan(html, actualTimeout, memoryLimitBytes);
    }

    private static string E(string text) => WebUtility.HtmlEncode(text);
}