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:
KenanZhuandClaude Opus 4.8 committed 2026-06-04 13:51:43 +08:00
1 parent a0cd5e32f4
commit 13c49d2087
2 files changed
+78 -15

No files matched your search

+48 -15
View File
@@ -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
<span class="hint">keys: d/m &nbsp; &larr;&rarr;</span>
</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="preview-area">
<div id="viewport" class="desktop">
@@ -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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
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, '');
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 = '';
};
})();
</script>
</body>
</html>