Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 14 additions & 140 deletions quickview-tool/public/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,11 @@ class QuickViewApp {
'json': 'json',
'md': 'md',
'svg': 'svg',
'css': 'css'
'css': 'css',
'yaml': 'yaml',
'yml': 'yaml',
'mmd': 'mermaid',
'mermaid': 'mermaid'
};

return classMap[ext] || 'file';
Expand Down Expand Up @@ -192,151 +196,21 @@ class QuickViewApp {
'.css': 'css',
'.json': 'json',
'.md': 'markdown',
'.svg': 'xml'
'.svg': 'xml',
'.yaml': 'yaml',
'.yml': 'yaml',
'.mmd': 'plaintext',
'.mermaid': 'plaintext'
};

return langMap[extension] || null;
}

updatePreviewPanel(content, extension, filename) {
const previewContent = document.getElementById('preview-content');

switch (extension) {
case '.html':
this.renderHTML(previewContent, content);
break;

case '.jsx':
this.renderReact(previewContent, content);
break;

case '.py':
this.renderPythonPreview(previewContent, content, filename);
break;

case '.svg':
this.renderSVG(previewContent, content);
break;

case '.md':
this.renderMarkdown(previewContent, content);
break;

case '.json':
this.renderJSON(previewContent, content);
break;

default:
this.renderText(previewContent, content);
}
}

renderHTML(container, content) {
const iframe = document.createElement('iframe');
iframe.className = 'preview-iframe';
iframe.srcdoc = content;

container.innerHTML = '';
container.appendChild(iframe);
}

renderReact(container, content) {
try {
// Create a wrapper for React component
const wrapper = document.createElement('div');
wrapper.id = 'react-preview';
container.innerHTML = '';
container.appendChild(wrapper);

// Transform JSX using Babel
const transformed = Babel.transform(content, {
presets: ['react']
}).code;

// Create and execute the component
const script = document.createElement('script');
script.textContent = `
try {
${transformed}

// Try to find and render the component
const componentName = Object.keys(window).find(key =>
typeof window[key] === 'function' &&
key[0] === key[0].toUpperCase()
);

if (componentName) {
const Component = window[componentName];
ReactDOM.render(React.createElement(Component), document.getElementById('react-preview'));
}
} catch (error) {
document.getElementById('react-preview').innerHTML =
'<div class="error">React Error: ' + error.message + '</div>';
}
`;

document.head.appendChild(script);
setTimeout(() => document.head.removeChild(script), 100);

} catch (error) {
container.innerHTML = `<div class="error">Failed to render React component: ${error.message}</div>`;
}
}

renderPythonPreview(container, content, filename) {
container.innerHTML = `
<div style="padding: 20px; color: #333;">
<h3>Python Script: ${filename}</h3>
<p>Click "Run" to execute this Python script and see the output.</p>
<div style="margin-top: 20px; padding: 15px; background: #f5f5f5; border-radius: 6px;">
<strong>Script Preview:</strong>
<pre style="margin-top: 10px; overflow-x: auto;">${this.escapeHtml(content.substring(0, 500))}${content.length > 500 ? '...' : ''}</pre>
</div>
</div>
`;
}

renderSVG(container, content) {
container.innerHTML = `
<div style="padding: 20px; text-align: center; background: white;">
${content}
</div>
`;
}

renderMarkdown(container, content) {
// Simple markdown rendering (would use marked.js in production)
const html = content
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
.replace(/\*\*(.*)\*\*/gim, '<strong>$1</strong>')
.replace(/\*(.*)\*/gim, '<em>$1</em>')
.replace(/\n/gim, '<br>');

container.innerHTML = `<div style="padding: 20px; color: #333;">${html}</div>`;
}

renderJSON(container, content) {
try {
const parsed = JSON.parse(content);
const formatted = JSON.stringify(parsed, null, 2);
container.innerHTML = `
<div style="padding: 20px; color: #333;">
<pre style="background: #f5f5f5; padding: 15px; border-radius: 6px; overflow-x: auto;">${this.escapeHtml(formatted)}</pre>
</div>
`;
} catch (error) {
container.innerHTML = `<div class="error">Invalid JSON: ${error.message}</div>`;
}
}

renderText(container, content) {
container.innerHTML = `
<div style="padding: 20px; color: #333;">
<pre style="white-space: pre-wrap; font-family: monospace;">${this.escapeHtml(content)}</pre>
</div>
`;
// Delegate to the modular renderer registry.
// To add a new renderer: create a file in public/renderers/ and add a <script> tag in index.html.
window.RendererRegistry.render(previewContent, content, extension, filename);
}

updateActionButtons(extension) {
Expand Down
32 changes: 30 additions & 2 deletions quickview-tool/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,33 @@
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<!-- marked.js for Markdown rendering -->
<script src="https://cdn.jsdelivr.net/npm/marked@5/marked.min.js"></script>
<!-- Mermaid.js for diagram rendering -->
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
<script>
// Initialise Mermaid with a dark theme to match the app
if (window.mermaid) {
mermaid.initialize({
startOnLoad: false,
theme: 'dark',
securityLevel: 'loose',
fontFamily: 'Inter, system-ui, sans-serif'
});
}
</script>
<link rel="stylesheet" href="style.css">
<!-- Renderer modules — registry must be first, text (fallback) must be last -->
<script src="renderers/registry.js"></script>
<script src="renderers/html-renderer.js"></script>
<script src="renderers/react-renderer.js"></script>
<script src="renderers/svg-renderer.js"></script>
<script src="renderers/json-renderer.js"></script>
<script src="renderers/mermaid-renderer.js"></script>
<script src="renderers/markdown-renderer.js"></script>
<script src="renderers/python-renderer.js"></script>
<script src="renderers/yaml-renderer.js"></script>
<script src="renderers/text-renderer.js"></script>
</head>
<body class="bg-background text-foreground h-screen overflow-hidden font-sans">
<div class="flex flex-col h-screen">
Expand Down Expand Up @@ -103,8 +129,10 @@ <h3 class="text-xs font-medium uppercase tracking-wider text-gray-400 mb-3">Supp
<li class="text-sm text-gray-600">⚛️ React Components (JSX)</li>
<li class="text-sm text-gray-600">🐍 Python Scripts</li>
<li class="text-sm text-gray-600">🎨 SVG Graphics</li>
<li class="text-sm text-gray-600">📝 Markdown</li>
<li class="text-sm text-gray-600">📊 JSON/YAML</li>
<li class="text-sm text-gray-600">📝 Markdown (+ Mermaid diagrams)</li>
<li class="text-sm text-gray-600">📊 JSON (interactive tree view)</li>
<li class="text-sm text-gray-600">📄 YAML/YML</li>
<li class="text-sm text-gray-600">🔀 Mermaid diagrams (.mmd)</li>
</ul>
</div>
</div>
Expand Down
16 changes: 16 additions & 0 deletions quickview-tool/public/renderers/html-renderer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* HTML Renderer — renders .html files in a sandboxed iframe.
*/
RendererRegistry.register({
name: 'html',
extensions: ['.html'],
priority: 10,

render(container, content) {
const iframe = document.createElement('iframe');
iframe.className = 'preview-iframe';
iframe.srcdoc = content;
container.innerHTML = '';
container.appendChild(iframe);
}
});
128 changes: 128 additions & 0 deletions quickview-tool/public/renderers/json-renderer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/**
* JSON Renderer — renders JSON as an interactive, collapsible tree view.
* Click the ▾/▸ toggle to expand or collapse objects and arrays.
*/
RendererRegistry.register({
name: 'json',
extensions: ['.json'],
priority: 10,

render(container, content) {
let parsed;
try {
parsed = JSON.parse(content);
} catch (err) {
container.innerHTML = `<div class="error">Invalid JSON: ${this._escapeHtml(err.message)}</div>`;
return;
}

container.innerHTML = '';
const viewer = document.createElement('div');
viewer.className = 'json-viewer';
viewer.appendChild(this._buildNode(parsed, null, true));
container.appendChild(viewer);
},

/**
* Recursively build a DOM node for a JSON value.
* @param {*} value — the JSON value
* @param {string|number|null} key — the key for this value, or null if root
* @param {boolean} isRoot — whether this is the top-level call
*/
_buildNode(value, key, isRoot) {
const type = value === null ? 'null' : Array.isArray(value) ? 'array' : typeof value;
const row = document.createElement('div');
row.className = 'jv-row';

// Render the key label (skip for root)
if (key !== null) {
const keyEl = document.createElement('span');
keyEl.className = 'jv-key';
keyEl.textContent = JSON.stringify(key) + ': ';
row.appendChild(keyEl);
}

if (type === 'null') {
const val = document.createElement('span');
val.className = 'jv-null';
val.textContent = 'null';
row.appendChild(val);
return row;
}

if (type === 'object' || type === 'array') {
const entries = type === 'array'
? value.map((v, i) => [i, v])
: Object.entries(value);
const count = entries.length;
const brackets = type === 'array' ? ['[', ']'] : ['{', '}'];

// Build summary for collapsed state
const previewKeys = type === 'array'
? `Array(${count})`
: `{ ${Object.keys(value).slice(0, 3).map(k => JSON.stringify(k)).join(', ')}${count > 3 ? ', …' : ''} }`;

const toggle = document.createElement('span');
toggle.className = 'jv-toggle';
toggle.textContent = '▾';
toggle.title = 'Click to collapse/expand';

const openBracket = document.createElement('span');
openBracket.className = 'jv-bracket';
openBracket.textContent = brackets[0];

const summary = document.createElement('span');
summary.className = 'jv-summary';
summary.textContent = ' ' + previewKeys;
summary.style.display = 'none';

const children = document.createElement('div');
children.className = 'jv-children';

entries.forEach(([k, v]) => {
children.appendChild(this._buildNode(v, k, false));
});

const closeRow = document.createElement('div');
closeRow.className = 'jv-row jv-close-row';
const closeBracket = document.createElement('span');
closeBracket.className = 'jv-bracket';
closeBracket.textContent = brackets[1];
closeRow.appendChild(closeBracket);

// Count badge
const badge = document.createElement('span');
badge.className = 'jv-count';
badge.textContent = ` // ${count} ${type === 'array' ? 'item' : 'key'}${count !== 1 ? 's' : ''}`;
openBracket.appendChild(badge);

toggle.addEventListener('click', () => {
const isCollapsed = children.style.display === 'none';
children.style.display = isCollapsed ? '' : 'none';
closeRow.style.display = isCollapsed ? '' : 'none';
summary.style.display = isCollapsed ? 'none' : '';
toggle.textContent = isCollapsed ? '▾' : '▸';
});

row.appendChild(toggle);
row.appendChild(openBracket);
row.appendChild(summary);
row.appendChild(children);
row.appendChild(closeRow);
} else {
// Primitive value
const val = document.createElement('span');
val.className = `jv-${type}`;
val.textContent = JSON.stringify(value);
row.appendChild(val);
}

return row;
},

_escapeHtml(text) {
const d = document.createElement('div');
d.textContent = String(text);
return d.innerHTML;
}
});
Loading