fix(preview): static-notice, simplify transform, HTML-safe rendering
- 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 <noreply@anthropic.com>
This commit is contained in:
1 parent
a0cd5e32f4
commit
13c49d2087
2 files changed
+78
-15
No files matched your search
+48
-15
@@ -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}
|
.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}
|
.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)}}
|
@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{width:4px}
|
||||||
#panel::-webkit-scrollbar-track{background:transparent}
|
#panel::-webkit-scrollbar-track{background:transparent}
|
||||||
@@ -100,6 +101,8 @@ body{font-family:system-ui,-apple-system,sans-serif;background:var(--bg);color:v
|
|||||||
<span class="hint">keys: d/m ←→</span>
|
<span class="hint">keys: d/m ←→</span>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<div id="static-notice">Gmail / Outlook simulation is approximate. For accurate rendering with CSS inlining, use <code style="background:rgba(251,191,36,0.1);padding:1px 6px;border-radius:3px;">cd tools && go run . dev</code></div>
|
||||||
|
|
||||||
<div id="main">
|
<div id="main">
|
||||||
<div id="preview-area">
|
<div id="preview-area">
|
||||||
<div id="viewport" class="desktop">
|
<div id="viewport" class="desktop">
|
||||||
@@ -153,25 +156,14 @@ let rendered = null; // window.__RENDERED__
|
|||||||
// Client transforms
|
// Client transforms
|
||||||
//
|
//
|
||||||
function transform(html, mode) {
|
function transform(html, mode) {
|
||||||
if(mode==='modern') return html;
|
|
||||||
if(mode==='raw') {
|
if(mode==='raw') {
|
||||||
const e = html.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
const e = html.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||||
return '<!DOCTYPE html><html><head><style>body{background:#0d1117;color:#c9d1d9;font-family:Consolas,monospace;padding:16px;margin:0}pre{white-space:pre-wrap;word-break:break-all;font-size:11px;line-height:1.6;margin:0}</style></head><body><pre>'+e+'</pre></body></html>';
|
return '<!DOCTYPE html><html><head><style>body{background:#0d1117;color:#c9d1d9;font-family:Consolas,monospace;padding:16px;margin:0}pre{white-space:pre-wrap;word-break:break-all;font-size:11px;line-height:1.6;margin:0}</style></head><body><pre>'+e+'</pre></body></html>';
|
||||||
}
|
}
|
||||||
let out = html.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '');
|
// Modern, Gmail, Outlook — all render the same raw HTML.
|
||||||
if(mode==='gmail') {
|
// Use the dev server (go run . dev) with Juice CSS inlining
|
||||||
out = out.replace(/font-family:'Poppins'/gi,'font-family:Arial,sans-serif');
|
// for accurate email client simulation.
|
||||||
out = out.replace(/font-family:'JetBrains Mono'/gi,'font-family:Consolas,monospace');
|
return html;
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -303,6 +295,47 @@ document.addEventListener('keydown',e=>{
|
|||||||
}
|
}
|
||||||
render();
|
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 = '';
|
||||||
|
};
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -165,6 +165,11 @@ func renderTheme(themeDir, themeName string, cfg *config.TemplatesConfig) ThemeS
|
|||||||
}
|
}
|
||||||
data["locale"] = Locale{}
|
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
|
// Parse and execute
|
||||||
tmpl, err := template.New(tplID).Funcs(TemplateFuncs).Parse(string(tmplContent))
|
tmpl, err := template.New(tplID).Funcs(TemplateFuncs).Parse(string(tmplContent))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -276,6 +281,31 @@ func PrintDetailedSummary(result *PreviewResult, themesDir string, cfg *config.T
|
|||||||
fmt.Println()
|
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 {
|
func formatSize(bytes int64) string {
|
||||||
const unit = 1024
|
const unit = 1024
|
||||||
if bytes < unit {
|
if bytes < unit {
|
||||||
|
|||||||
Reference in new issue
Block a user