skill-creator: Add markdown rendering to eval viewer
Preflight Checklist
- [x] I have searched existing requests and this feature hasn't been requested yet
- [x] This is a single feature request (not multiple features)
Problem Statement
The eval viewer currently renders .md files as plain < pre > text. When evaluating skills that produce markdown (Obsidian pages, docs, READMEs), it's much easier to review rendered output. This patch adds rendered markdown preview
with a Raw/Rendered toggle, using marked.js + DOMPurify for safe HTML generation.
Proposed Solution
Feature: Rendered markdown preview in eval viewer
============================================================
This patch adds markdown rendering support to the skill-creator eval viewer.
Markdown (.md) files are now rendered with proper formatting by default,
with a Rendered/Raw toggle button to switch between views.
Dependencies added (CDN):
- marked.js — lightweight markdown parser
- DOMPurify — HTML sanitizer to prevent XSS from parsed markdown
File: eval-viewer/viewer.html
------------------------------------------------------------
- Add CDN script tags after the SheetJS import:
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js"></script>
- Add CSS (after
.output-file-content .download-link:hoverblock):
.output-file-header .md-toggle {
font-size: 0.7rem;
color: var(--accent);
cursor: pointer;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-weight: 500;
opacity: 0.8;
background: none;
border: 1px solid var(--accent);
border-radius: 3px;
padding: 0.15rem 0.4rem;
margin-right: 0.5rem;
}
.output-file-header .md-toggle:hover {
opacity: 1;
background: var(--accent);
color: white;
}
.output-file-header .md-toggle.active {
background: var(--accent);
color: white;
opacity: 1;
}
.md-rendered {
font-size: 0.875rem;
line-height: 1.6;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.md-rendered h1 { font-size: 1.4rem; margin: 1rem 0 0.5rem; border-bottom: 1px solid var(--border); padding-bottom: 0.3rem; }
.md-rendered h2 { font-size: 1.15rem; margin: 0.8rem 0 0.4rem; }
.md-rendered h3 { font-size: 1rem; margin: 0.6rem 0 0.3rem; }
.md-rendered p { margin: 0.4rem 0; }
.md-rendered ul, .md-rendered ol { margin: 0.4rem 0 0.4rem 1.5rem; }
.md-rendered li { margin: 0.2rem 0; }
.md-rendered code { background: var(--bg); padding: 0.1rem 0.3rem; border-radius: 3px; font-size: 0.8rem; font-family: 'SF Mono', Consolas, monospace; }
.md-rendered pre { background: var(--bg); padding: 0.75rem; border-radius: 4px; overflow-x: auto; margin: 0.5rem 0; }
.md-rendered pre code { background: none; padding: 0; }
.md-rendered a { color: var(--accent); }
.md-rendered blockquote { border-left: 3px solid var(--border); padding-left: 0.75rem; color: var(--text-muted); margin: 0.5rem 0; }
.md-rendered table { border-collapse: collapse; font-size: 0.8125rem; width: 100%; margin: 0.5rem 0; }
.md-rendered table td, .md-rendered table th { border: 1px solid var(--border); padding: 0.375rem 0.5rem; text-align: left; }
.md-rendered table th { background: var(--bg); font-weight: 600; }
.md-rendered hr { border: none; border-top: 1px solid var(--border); margin: 0.75rem 0; }
.md-rendered strong { font-weight: 600; }
- Add JavaScript helper function (before the XLSX rendering section):
// ---- Markdown rendering with raw/rendered toggle ----
function renderMarkdownWithToggle(container, mdText, toggleBtn) {
const renderedDiv = document.createElement("div");
renderedDiv.className = "md-rendered";
const rawHtml = marked.parse(mdText);
const cleanHtml = typeof DOMPurify !== "undefined" ? DOMPurify.sanitize(rawHtml) : mdText;
renderedDiv.appendChild(document.createRange().createContextualFragment(cleanHtml));
container.appendChild(renderedDiv);
const rawPre = document.createElement("pre");
rawPre.textContent = mdText;
rawPre.style.display = "none";
container.appendChild(rawPre);
if (toggleBtn) {
toggleBtn.addEventListener("click", function() {
if (this.dataset.mode === "rendered") {
renderedDiv.style.display = "none";
rawPre.style.display = "";
this.textContent = "Raw";
this.dataset.mode = "raw";
this.classList.remove("active");
} else {
renderedDiv.style.display = "";
rawPre.style.display = "none";
this.textContent = "Rendered";
this.dataset.mode = "rendered";
this.classList.add("active");
}
});
}
}
- Modify renderOutputs() — replace the file header + text rendering block:
In both the main renderOutputs function and the previous-outputs rendering
section, wrap the download button in a headerActions span that also includes
a toggle button for .md files, then use renderMarkdownWithToggle instead of
a plain <pre> tag when the file is markdown:
// Detect markdown files
const isMd = file.type === "text" && file.name.endsWith(".md") && typeof marked !== "undefined";
// Add toggle button to header for .md files
if (isMd) {
const toggleBtn = document.createElement("button");
toggleBtn.className = "md-toggle active";
toggleBtn.textContent = "Rendered";
toggleBtn.dataset.mode = "rendered";
headerActions.appendChild(toggleBtn);
}
// In the content rendering block:
if (file.type === "text") {
if (isMd) {
renderMarkdownWithToggle(content, file.content, header.querySelector(".md-toggle"));
} else {
const pre = document.createElement("pre");
pre.textContent = file.content;
content.appendChild(pre);
}
}
This pattern is applied in two locations: the main renderOutputs() function
and the previous-outputs collapsible section.
Alternative Solutions
_No response_
Priority
Medium - Would be very helpful
Feature Category
CLI commands and flags
Use Case Example
Evaluating skills that produce markdown (Obsidian pages, docs, READMEs), it's much easier to review rendered output.
Additional Context
_No response_
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗