From 13c49d20870e1d2a49cc8154d6a331c81a522478 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Thu, 4 Jun 2026 13:51:43 +0800 Subject: [PATCH] fix(preview): static-notice, simplify transform, HTML-safe rendering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add static-notice banner: 'Gmail/Outlook simulation is approximate. Use dev mode for accurate rendering.' Hidden in dev mode via WebSocket try/catch, auto-shown in file:// mode on WS failure. - Simplify transform(): Gmail/Outlook modes now return same HTML as Modern — the dev server with Juice provides actual client sim. - Add markSafeHTML(): converts RenderedNote/Body/RenderedContent from plain string to template.HTML so markdown-rendered HTML content is not escaped by Go's html/template engine. Co-Authored-By: Claude Opus 4.8 --- preview/index.html | 63 +++++++++++++++++++++++++++++++---------- tools/preview/engine.go | 30 ++++++++++++++++++++ 2 files changed, 78 insertions(+), 15 deletions(-) diff --git a/preview/index.html b/preview/index.html index 78829c4..908dc3f 100644 --- a/preview/index.html +++ b/preview/index.html @@ -68,6 +68,7 @@ body{font-family:system-ui,-apple-system,sans-serif;background:var(--bg);color:v .loading-wrap{display:flex;align-items:center;justify-content:center;height:100%;color:var(--muted);font-size:13px;flex-direction:column;gap:8px} .spinner{width:20px;height:20px;border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .6s linear infinite} @keyframes spin{to{transform:rotate(360deg)}} +#static-notice{display:none;background:rgba(251,191,36,0.08);border-bottom:1px solid rgba(251,191,36,0.15);padding:6px 16px;font-size:11px;color:rgba(251,191,36,0.7);text-align:center;flex-shrink:0} #panel::-webkit-scrollbar{width:4px} #panel::-webkit-scrollbar-track{background:transparent} @@ -100,6 +101,8 @@ body{font-family:system-ui,-apple-system,sans-serif;background:var(--bg);color:v keys: d/m   ←→ +
Gmail / Outlook simulation is approximate. For accurate rendering with CSS inlining, use cd tools && go run . dev
+
@@ -153,25 +156,14 @@ let rendered = null; // window.__RENDERED__ // Client transforms // function transform(html, mode) { - if(mode==='modern') return html; if(mode==='raw') { const e = html.replace(/&/g,'&').replace(//g,'>'); return '
'+e+'
'; } - let out = html.replace(/]*>[\s\S]*?<\/style>/gi, ''); - if(mode==='gmail') { - out = out.replace(/font-family:'Poppins'/gi,'font-family:Arial,sans-serif'); - out = out.replace(/font-family:'JetBrains Mono'/gi,'font-family:Consolas,monospace'); - } - if(mode==='outlook') { - const strip=[/border-radius:[^;"']+/gi,/box-shadow:[^;"']+/gi,/linear-gradient\([^)]*\)/gi,/transition:[^;"']+/gi,/letter-spacing:[^;"']+/gi,/font-variant:small-caps/gi,/-webkit-background-clip:text/gi,/-webkit-text-fill-color:transparent/gi,/overflow:hidden/gi]; - strip.forEach(r=>{out=out.replace(r,'')}); - out=out.replace(/background:linear-gradient\([^)]*\)/gi,'background:#f0f0f0'); - out=out.replace(/style="[^"]*"/g,m=>m.replace(/;\s*;/g,';')); - out=out.replace(/font-family:Georgia/gi,'font-family:Georgia,serif'); - out=out.replace(/font-family:'Charter'/gi,'font-family:Georgia,serif'); - } - return out; + // Modern, Gmail, Outlook — all render the same raw HTML. + // Use the dev server (go run . dev) with Juice CSS inlining + // for accurate email client simulation. + return html; } // @@ -303,6 +295,47 @@ document.addEventListener('keydown',e=>{ } render(); })(); + +// ── Dev-server live-reload (WebSocket) ── +(function(){ + const notice = document.getElementById('static-notice'); + let ws; + try { + const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; + ws = new WebSocket(proto + '//' + location.host + '/ws'); + } catch(e) { + // static mode (file://) — WebSocket constructor throws + if (notice) notice.style.display = ''; + return; + } + ws.onmessage = function(e) { + try { + const msg = JSON.parse(e.data); + if (msg.type === 'reload') { + const script = document.createElement('script'); + script.src = 'rendered.js?v=' + Date.now(); + script.onload = function() { + if (window.__RENDERED__) rendered = window.__RENDERED__; + if (window.__REGISTRY__) REGISTRY = window.__REGISTRY__; + if (window.__PARAMS__) PARAMS = window.__PARAMS__; + render(); + }; + document.head.appendChild(script); + } + } catch(_) {} + }; + ws.onopen = function() { + if (notice) notice.style.display = 'none'; + console.log('[dev] live-reload connected'); + }; + ws.onclose = function() { + if (notice) notice.style.display = ''; + console.log('[dev] live-reload disconnected'); + }; + ws.onerror = function() { + if (notice) notice.style.display = ''; + }; +})(); diff --git a/tools/preview/engine.go b/tools/preview/engine.go index 319a1c0..12e08e4 100644 --- a/tools/preview/engine.go +++ b/tools/preview/engine.go @@ -165,6 +165,11 @@ func renderTheme(themeDir, themeName string, cfg *config.TemplatesConfig) ThemeS } data["locale"] = Locale{} + // Convert HTML-safe fields from string to template.HTML so Go's + // html/template renders them raw instead of escaping. Matches Gitea's + // behavior where RenderedNote / Body / RenderedContent are template.HTML. + markSafeHTML(data) + // Parse and execute tmpl, err := template.New(tplID).Funcs(TemplateFuncs).Parse(string(tmplContent)) if err != nil { @@ -276,6 +281,31 @@ func PrintDetailedSummary(result *PreviewResult, themesDir string, cfg *config.T fmt.Println() } +// markSafeHTML recursively converts known HTML-containing string fields to +// template.HTML so Go's html/template renders them raw instead of escaping. +// These fields mirror Gitea's data model where rendered markdown is typed as +// template.HTML: Release.RenderedNote, Body (comment content), and +// ReviewComments[].RenderedContent. +func markSafeHTML(data map[string]any) { + for k, v := range data { + switch val := v.(type) { + case string: + if k == "RenderedNote" || k == "Body" || k == "RenderedContent" { + data[k] = template.HTML(val) + } + case map[string]any: + markSafeHTML(val) + case []any: + for i, item := range val { + if m, ok := item.(map[string]any); ok { + markSafeHTML(m) + val[i] = m + } + } + } + } +} + func formatSize(bytes int64) string { const unit = 1024 if bytes < unit {