feat: Add Forge Document mode to Markdown Converter

- Backend:
  - Added 'forge' output format support (maps to HTML with Forge theme).
  - Implemented 'forge' theme with Montserrat font and white-paper styling.
  - Fixed 'Plain Text' mode not returning output.
  - Added fallback 'output' return when markdown library is missing.
- Frontend:
  - Added 'Forge Document' as the default output format.
  - Implemented 'Copy Formatted' button for rich text clipboard support (Word/Excel compatible).
  - Switched to single-column layout for better document visibility.
  - Used iframe for document preview to isolate styles and prevent layout issues.
This commit is contained in:
DJP 2025-12-11 16:08:24 -05:00
parent d7852fc399
commit 3f88af3258
2 changed files with 88 additions and 37 deletions

View file

@ -415,6 +415,10 @@ async def convert_markdown(
Dictionary with converted content
"""
try:
if output_format == "forge":
output_format = "html"
theme = "forge"
import markdown
from markdown.extensions import tables, fenced_code, toc
@ -429,12 +433,20 @@ async def convert_markdown(
])
html = md.convert(content)
# Add basic styling
# Define styles based on theme
extra_head = ""
font_family = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"
if theme == "forge":
extra_head = '<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;700&display=swap" rel="stylesheet">'
font_family = "'Montserrat', sans-serif"
styled_html = f"""<!DOCTYPE html>
<html>
<head>
{extra_head}
<style>
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; max-width: 800px; margin: 0 auto; padding: 20px; }}
body {{ font-family: {font_family}; line-height: 1.6; max-width: 800px; margin: 0 auto; padding: 20px; }}
code {{ background: #f4f4f4; padding: 2px 6px; border-radius: 3px; }}
pre {{ background: #f4f4f4; padding: 16px; border-radius: 6px; overflow-x: auto; }}
table {{ border-collapse: collapse; width: 100%; }}
@ -470,6 +482,7 @@ blockquote {{ border-left: 4px solid #ddd; margin: 0; padding-left: 16px; color:
return {
"success": True,
"output": text.strip(),
"content": text.strip(),
"format": "plain"
}
@ -484,6 +497,7 @@ blockquote {{ border-left: 4px solid #ddd; margin: 0; padding-left: 16px; color:
# Fallback without markdown library
return {
"success": True,
"output": content,
"content": content,
"format": output_format,
"note": "markdown library not installed"

View file

@ -6,18 +6,20 @@ import { FileText, Download, Sparkles, Copy } from 'lucide-react';
import { modulesApi } from '@/lib/api';
const outputFormats = [
{ value: 'forge', label: 'Forge Document' },
{ value: 'html', label: 'HTML' },
{ value: 'plain', label: 'Plain Text' },
];
const themes = [
{ value: 'forge', label: 'Forge' },
{ value: 'github', label: 'GitHub' },
{ value: 'default', label: 'Default' },
];
export default function MarkdownConverterPage() {
const [content, setContent] = useState('');
const [outputFormat, setOutputFormat] = useState('html');
const [outputFormat, setOutputFormat] = useState('forge');
const [theme, setTheme] = useState('github');
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<any>(null);
@ -48,7 +50,26 @@ export default function MarkdownConverterPage() {
};
const handleCopy = () => {
if (result?.output) {
if (!result?.output) return;
if (outputFormat === 'forge') {
try {
const blob = new Blob([result.output], { type: 'text/html' });
const plainBlob = new Blob([result.output], { type: 'text/plain' });
navigator.clipboard.write([
new ClipboardItem({
'text/html': blob,
'text/plain': plainBlob,
})
]);
toast.success('Copied formatted text!');
} catch (e) {
console.error(e);
navigator.clipboard.writeText(result.output);
toast.success('Copied HTML source (Rich copy failed)');
}
} else {
navigator.clipboard.writeText(result.output);
toast.success('Copied to clipboard!');
}
@ -81,7 +102,7 @@ export default function MarkdownConverterPage() {
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div className="grid grid-cols-1 gap-8">
{/* Controls */}
<div className="space-y-6">
<div>
@ -149,39 +170,55 @@ export default function MarkdownConverterPage() {
{result?.output ? (
<>
<div className="bg-forge-dark rounded-lg p-4 border border-gray-800">
<div className="flex justify-between items-center mb-2">
<span className="text-xs text-gray-500 font-mono">
{outputFormat === 'html' ? 'HTML Output' : 'Plain Text'}
</span>
<div className="flex gap-2">
<button
onClick={handleCopy}
className="text-xs text-forge-yellow hover:text-forge-yellow/80 flex items-center gap-1"
>
<Copy className="w-3 h-3" />
Copy
</button>
<button
onClick={handleDownload}
className="text-xs text-forge-yellow hover:text-forge-yellow/80 flex items-center gap-1"
>
<Download className="w-3 h-3" />
Download
</button>
</div>
</div>
<pre className="text-sm text-gray-300 overflow-x-auto max-h-[400px]">
<code>{result.output}</code>
</pre>
</div>
{outputFormat === 'html' && (
{outputFormat !== 'forge' && (
<div className="bg-forge-dark rounded-lg p-4 border border-gray-800">
<p className="text-xs text-gray-500 mb-2">Preview</p>
<div
className="prose prose-invert max-w-none"
dangerouslySetInnerHTML={{ __html: result.output }}
<div className="flex justify-between items-center mb-2">
<span className="text-xs text-gray-500 font-mono">
{outputFormat === 'html' ? 'HTML Output' : 'Plain Text'}
</span>
<div className="flex gap-2">
<button
onClick={handleCopy}
className="text-xs text-forge-yellow hover:text-forge-yellow/80 flex items-center gap-1"
>
<Copy className="w-3 h-3" />
Copy
</button>
<button
onClick={handleDownload}
className="text-xs text-forge-yellow hover:text-forge-yellow/80 flex items-center gap-1"
>
<Download className="w-3 h-3" />
Download
</button>
</div>
</div>
<pre className="text-sm text-gray-300 overflow-x-auto max-h-[400px]">
<code>{result.output}</code>
</pre>
</div>
)}
{(outputFormat === 'html' || outputFormat === 'forge') && (
<div className={`rounded-lg p-8 border border-gray-800 ${outputFormat === 'forge' ? 'bg-white text-black shadow-xl' : 'bg-forge-dark'
}`}>
<div className="flex justify-between items-center mb-4 border-b border-gray-200 pb-2">
<p className={`text-xs font-semibold ${outputFormat === 'forge' ? 'text-gray-500' : 'text-gray-500'}`}>Preview</p>
{outputFormat === 'forge' && (
<button
onClick={handleCopy}
className="text-xs flex items-center gap-2 font-medium bg-gray-100 hover:bg-gray-200 text-gray-800 px-3 py-1.5 rounded-md transition-colors border border-gray-300"
title="Copy styled content for Word/Excel"
>
<Copy className="w-3 h-3" />
Copy Formatted
</button>
)}
</div>
<iframe
srcDoc={result.output}
className="w-full h-[800px] bg-white rounded-lg shadow-inner"
title="Document Preview"
/>
</div>
)}