A modern, browser-based real-time transformer application that allows users to edit structured XML documents and see instant HTML preview rendering. The application focuses on document formatting (e.g., cover letters, resumes) with a clean, intuitive interface. This is a Proof of Concept (PoC) version focusing on core functionality with code-level template extensibility.
- Content creators and writers
- HR professionals and recruiters
- Document designers
- Developers working with XML/HTML transformations
- Framework: Nuxt.js (latest version via
npm create nuxt@latest) - Language: TypeScript
- UI Components: @nuxt/ui for application chrome (buttons, inputs, modals, alerts)
- Styling: Vue Scoped CSS for document templates (exportable content)
- XML Parsing: DOMParser (native browser API)
- Code Editor: Monaco Editor via
nuxt-monaco-editor
-
Left Panel: XML Editor
- Syntax highlighting for XML
- Line numbers
- Auto-indentation
- Real-time validation
- Resizable panel width
-
Right Panel: HTML Preview
- Live rendering of transformed HTML using Vue templates
- Styled according to the active template's CSS
- Scrollable content area
- Responsive layout
- Initial State: Application loads with sample XML (
public/samples/cover-letter.xml) pre-populated in editor on first load - Instant Updates: Changes in XML editor trigger immediate preview updates
- Debouncing: Implement 300ms debounce to optimize performance
- Error Handling: Display friendly error messages for invalid XML
- Syntax Validation: Highlight XML syntax errors in the editor
Based on docs/sample/cover-letter.xml, the application should support the following XML structure:
<applicationDocument>
<applicant>
<name>...</name>
<address>
<street>...</street>
<city>...</city>
<state>...</state>
<zipCode>...</zipCode>
</address>
<contactInformation>
<phone>...</phone>
<email>...</email>
</contactInformation>
</applicant>
<date>...</date>
<recipient>
<position>...</position>
<company>...</company>
<address>...</address>
</recipient>
<letter>
<salutation>...</salutation>
<introduction>...</introduction>
<experienceSection>
<experience>
<employer>...</employer>
<achievements>
<achievement>...</achievement>
</achievements>
</experience>
</experienceSection>
<motivation>...</motivation>
<closing>...</closing>
<signature>...</signature>
</letter>
</applicationDocument>Note: The XML structure is standardized and does NOT include a formatStyle attribute. Template selection is handled at the code level only.
The application uses Vue Single File Components (SFCs) as templates:
- Each template is a
.vuefile with its own HTML structure and scoped CSS - Templates render the same XML data but with completely different HTML structures
- CSS is template-specific and swaps out entirely when templates change
- PoC Scope: Templates are swapped by changing a configuration constant in code (no UI control)
Example Template Mapping (Modern Template):
| XML Element | Vue Template Output | CSS Class |
|---|---|---|
<applicationDocument> |
<article> |
application-document |
<applicant> |
<div> or <header> |
applicant |
<name> |
<h1> |
applicant-name |
<address> |
<address> |
applicant-address / recipient-address |
<contactInformation> |
<p> |
contact-information |
<email> |
<a :href="mailto:..."> |
email |
<phone> |
<span> |
phone |
<date> |
<time> |
document-date |
<achievements> |
<ul> with v-for |
achievements |
<achievement> |
<li> |
achievement |
Different templates may use completely different HTML structures (e.g., Classic template might use <table>, Minimal template might use <div> only).
- Pre-loaded Sample: Default cover letter XML loads automatically on app initialization
- Import XML: Upload XML file from local filesystem
- Export HTML: Download rendered HTML output only (as seen in preview) with embedded CSS
- Format XML: Auto-format/prettify XML button
- Clear Editor: Clear all content with confirmation
- Zoom Control: Scale preview (75%, 100%, 125%, 150%)
- Initial page load: < 2 seconds
- XML-to-HTML transformation: < 100ms for documents up to 50KB
- Smooth editing experience with no lag
- Chrome/Edge (latest 2 versions)
- Firefox (latest 2 versions)
- Safari (latest 2 versions)
- Desktop-first approach
- Minimum width: 1024px (tablet landscape)
- Stacked layout on mobile (< 768px): Editor on top, preview below
- WCAG 2.1 Level AA compliance
- Keyboard navigation support
- Screen reader compatibility
- Sufficient color contrast
- Minimalist: Clean, uncluttered interface
- Professional: Suitable for business documents
- Functional: Focus on content and usability
Application UI: Uses @nuxt/ui component library
- Headers, buttons, inputs, modals, alerts
- Consistent, accessible, professionally styled out-of-the-box
- Theme can be customized via Nuxt config if needed
Document Templates: Use scoped CSS for exportable content
- Each template (
/templates/{name}/styles.css) defines its own styling - Templates must not depend on @nuxt/ui (for standalone HTML export)
- Color palette and typography defined per template
- Default Modern template uses
docs/sample/styles.cssas reference
┌─────────────────────────────────────────────────────┐
│ Header (App Title + Actions) │
├──────────────────────┬──────────────────────────────┤
│ │ │
│ XML Editor │ HTML Preview │
│ (Monaco Editor) │ (Vue Template Render) │
│ │ │
│ [Format] [Clear] │ [Export HTML] [Zoom] │
│ │ │
└──────────────────────┴──────────────────────────────┘
/pages
index.vue # Main application page
/debug
parser.vue # XML parser demo page
template.vue # Template renderer demo page
editor.vue # Editor component demo page
preview.vue # Preview panel demo page
/components
XmlEditor.vue # XML editor (@nuxt/ui wrapper + Monaco)
PreviewPanel.vue # Preview container (@nuxt/ui wrapper)
AppHeader.vue # App header (@nuxt/ui components)
/templates
/modern
CoverLetterModern.vue # Modern template (default)
styles.css # Modern-specific styles (scoped CSS)
/classic
CoverLetterClassic.vue # Classic template
styles.css # Classic-specific styles (scoped CSS)
/minimal
CoverLetterMinimal.vue # Minimal template
styles.css # Minimal-specific styles (scoped CSS)
/composables
useTemplate.ts # Template selection config
useXmlParser.ts # XML parsing to data object
useExport.ts # HTML export functionality
/public
samples/
cover-letter.xml # Default XML sample (copied from docs/sample/)
// components/XmlEditor.vue
interface Props {
modelValue: string;
}
interface Emits {
(e: 'update:modelValue', value: string): void;
(e: 'error', error: string | null): void;
}// components/PreviewPanel.vue
interface Props {
xmlContent: string;
zoom: number;
}<!-- templates/modern/CoverLetterModern.vue -->
<template>
<article class="application-document">
<header>
<h1>{{ applicant.name }}</h1>
<address v-if="applicant.address">
{{ applicant.address.street }}, {{ applicant.address.city }},
{{ applicant.address.state }} {{ applicant.address.zipCode }}
</address>
<p class="contact-information">
<span v-if="applicant.contactInformation?.phone">
{{ applicant.contactInformation.phone }}
</span>
<a v-if="applicant.contactInformation?.email"
:href="`mailto:${applicant.contactInformation.email}`">
{{ applicant.contactInformation.email }}
</a>
</p>
</header>
<main class="letter">
<p class="salutation">{{ letter.salutation }}</p>
<p class="introduction">{{ letter.introduction }}</p>
<section v-if="letter.experienceSection" class="experience-section">
<p class="experience-employer">
<strong>{{ letter.experienceSection.experience.employer }}</strong>
</p>
<ul class="achievements">
<li v-for="(achievement, idx) in letter.experienceSection.experience.achievements"
:key="idx"
class="achievement">
{{ achievement }}
</li>
</ul>
</section>
<p class="motivation">{{ letter.motivation }}</p>
<p class="closing">{{ letter.closing }}</p>
<p class="signature">{{ letter.signature }}</p>
</main>
</article>
</template>
<script setup lang="ts">
interface Props {
applicant: {
name: string;
address?: { street: string; city: string; state: string; zipCode: string };
contactInformation?: { phone?: string; email?: string };
};
date: string;
recipient: {
position: string;
company: string;
address?: { street: string; city: string; state: string; zipCode: string };
};
letter: {
salutation: string;
introduction: string;
experienceSection?: {
experience: {
employer: string;
achievements: string[];
};
};
motivation: string;
closing: string;
signature: string;
};
}
defineProps<Props>();
</script>
<style scoped>
@import './styles.css';
</style>// composables/useTemplate.ts
import ModernTemplate from '~/templates/modern/CoverLetterModern.vue';
import ClassicTemplate from '~/templates/classic/CoverLetterClassic.vue';
import MinimalTemplate from '~/templates/minimal/CoverLetterMinimal.vue';
// PoC: Developer changes this constant to switch templates
const ACTIVE_TEMPLATE = 'modern';
const templates = {
modern: ModernTemplate,
classic: ClassicTemplate,
minimal: MinimalTemplate
};
export const useTemplate = () => {
const TemplateComponent = templates[ACTIVE_TEMPLATE];
const templateName = ACTIVE_TEMPLATE;
return {
TemplateComponent,
templateName
};
};// composables/useXmlParser.ts
export interface ParsedData {
applicant: {
name: string;
address?: { street: string; city: string; state: string; zipCode: string };
contactInformation?: { phone?: string; email?: string };
};
date: string;
recipient: {
position: string;
company: string;
address?: { street: string; city: string; state: string; zipCode: string };
};
letter: {
salutation: string;
introduction: string;
experienceSection?: {
experience: {
employer: string;
achievements: string[];
};
};
motivation: string;
closing: string;
signature: string;
};
}
export const useXmlParser = () => {
const parseXml = (xmlString: string): ParsedData | null => {
try {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlString, 'text/xml');
// Check for parsing errors
const parserError = xmlDoc.querySelector('parsererror');
if (parserError) {
throw new Error('Invalid XML');
}
// Extract data from XML
const getText = (selector: string): string => {
return xmlDoc.querySelector(selector)?.textContent?.trim() || '';
};
const achievements = Array.from(
xmlDoc.querySelectorAll('achievement')
).map(el => el.textContent?.trim() || '');
return {
applicant: {
name: getText('applicant name'),
address: {
street: getText('applicant address street'),
city: getText('applicant address city'),
state: getText('applicant address state'),
zipCode: getText('applicant address zipCode')
},
contactInformation: {
phone: getText('applicant contactInformation phone'),
email: getText('applicant contactInformation email')
}
},
date: getText('date'),
recipient: {
position: getText('recipient position'),
company: getText('recipient company'),
address: {
street: getText('recipient address street'),
city: getText('recipient address city'),
state: getText('recipient address state'),
zipCode: getText('recipient address zipCode')
}
},
letter: {
salutation: getText('letter salutation'),
introduction: getText('letter introduction'),
experienceSection: achievements.length > 0 ? {
experience: {
employer: getText('experience employer'),
achievements
}
} : undefined,
motivation: getText('letter motivation'),
closing: getText('letter closing'),
signature: getText('letter signature')
}
};
} catch (error) {
console.error('XML parsing error:', error);
return null;
}
};
const validateXml = (xmlString: string): { valid: boolean; error?: string } => {
try {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlString, 'text/xml');
const parserError = xmlDoc.querySelector('parsererror');
if (parserError) {
return { valid: false, error: 'Invalid XML syntax' };
}
return { valid: true };
} catch (error) {
return { valid: false, error: (error as Error).message };
}
};
return { parseXml, validateXml };
};// composables/useExport.ts
export const useExport = () => {
const exportHtml = (previewElementId: string, templateName: string) => {
const previewElement = document.getElementById(previewElementId);
if (!previewElement) return;
const htmlContent = previewElement.innerHTML;
// Get the active template's CSS
const styleElements = Array.from(document.querySelectorAll('style'))
.filter(style => style.textContent?.includes('application-document'))
.map(style => style.textContent)
.join('\n');
const fullHtml = `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Cover Letter</title>
<style>
${styleElements}
</style>
</head>
<body>
${htmlContent}
</body>
</html>`;
// Download
const blob = new Blob([fullHtml], { type: 'text/html' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `cover-letter-${templateName}.html`;
a.click();
URL.revokeObjectURL(url);
};
return { exportHtml };
};- Vue Composables: Use composables for shared logic (template, parser, export)
- Component State: Use
refandcomputedfor reactive state - No External Library: Keep it simple, no Pinia/Vuex for PoC
User Types → XmlEditor Component → v-model binding →
Debounced watcher (300ms) → useXmlParser composable →
Parsed Data Object → Template Component (via props) →
Vue renders HTML → PreviewPanel displays
As a user, I want to:
- See the sample cover letter XML pre-loaded when I open the app so I can start immediately
- Edit XML in a syntax-highlighted editor so I can create structured documents easily
- See real-time HTML preview so I can immediately visualize my changes
- Export the rendered HTML so I can use it in emails or documents
- Resize panels so I can focus on editing or preview as needed
- Receive clear error messages when XML is invalid so I can fix issues quickly
As a user, I want to: 7. Import my own XML files so I can edit existing documents 8. Format/prettify my XML so it's easier to read 9. Adjust preview zoom so I can see details or get an overview
As a developer, I want to: 10. Add new HTML/CSS templates by creating new Vue components in the codebase 11. Switch templates by changing a configuration constant 12. Have templates use completely different HTML structures while consuming the same XML data
- User engagement: Average session duration > 5 minutes
- Feature adoption: 100% of users see pre-loaded sample (default state)
- Error rate: < 5% of transformations result in errors
- Export usage: > 50% of sessions include an export action
- Ease of use rating: > 4.5/5
- Performance satisfaction: > 4.5/5
- Visual design rating: > 4/5
This project follows an agile MVP-based development approach. The implementation is broken down into 12 progressive MVPs, each delivering a working, testable component.
See: docs/MVP-PLAN.md for detailed breakdown of:
- 12 MVPs (MVP 1: Basic App Shell → MVP 12: Accessibility & Polish)
- Each MVP's goals, deliverables, and acceptance criteria
- MVP dependencies and testing strategy
- Demo pages at
/debug/*for component testing
Key Milestones:
- MVP 1-5: Foundation (parser, template, editor, preview components)
- MVP 6: Integration (dual-panel main app)
- MVP 7-9: Core features (export, editor actions, zoom)
- MVP 10-11: Enhancement (panel resize, additional templates)
- MVP 12: Production-ready (accessibility, cross-browser)
Development Principles:
- Each MVP is independently usable and testable
- Components built in demo pages (MVP 2-5) are reused in main app (MVP 6)
- Progressive enhancement: later MVPs build on earlier ones
- Demo pages remain as documentation at
/debug/*
- UI Template Switcher: Dropdown to switch templates in the UI (no longer code-only)
- Multi-document support: Tabs for multiple XML files
- Custom templates: User-uploadable Vue templates
- Version history: Undo/redo with history panel
- Export formats: PDF, DOCX, Markdown
- AI assistance: Auto-complete, suggestions
- Cloud storage (Google Drive, Dropbox)
- Template marketplace
- API for programmatic transformations
- Must be a pure frontend application (no backend)
- Must use Nuxt.js as the framework
- Must work entirely in the browser
- PoC scope: Template switching is code-level only (no UI control)
- Users have modern browsers with JavaScript enabled
- Target document size: < 100KB XML files
- Users have basic familiarity with XML structure
- Internet connection required for initial load (CDN resources)
- Developers can edit code to add/switch templates
- Backend database or user authentication
- Complex XML schemas beyond document formatting
- Real-time collaboration
- Mobile app versions
- UI-based template selection
- PDF export (can use browser print)
- User-editable CSS
| Risk | Impact | Mitigation |
|---|---|---|
| Large XML files cause performance issues | High | Implement virtual scrolling, size warnings |
| Complex XML breaks parser | Medium | Robust error handling, validation |
| Browser compatibility issues | Medium | Polyfills, progressive enhancement |
| User data loss on refresh | High | Auto-save to localStorage |
| Template switching requires code changes | Low | Acceptable for PoC; document clearly for developers |
See: docs/sample/cover-letter.xml
See: docs/sample/cover-letter.html (if available)
See: docs/sample/styles.css (used as reference for Modern template)
- Framework:
nuxt(latest) - XML Editor:
nuxt-monaco-editor - XML Parsing: Native
DOMParserAPI - Panel Resizing:
splitpanesor native CSS resize
Step 1: Create template directory
mkdir templates/my-templateStep 2: Create Vue component
<!-- templates/my-template/CoverLetterMyTemplate.vue -->
<template>
<!-- Your custom HTML structure -->
</template>
<script setup lang="ts">
defineProps<Props>(); // Use same Props interface
</script>
<style scoped>
@import './styles.css';
</style>Step 3: Create styles
/* templates/my-template/styles.css */
/* Your custom CSS */Step 4: Register template
// composables/useTemplate.ts
import MyTemplate from '~/templates/my-template/CoverLetterMyTemplate.vue';
const templates = {
modern: ModernTemplate,
classic: ClassicTemplate,
mytemplate: MyTemplate // Add here
};
const ACTIVE_TEMPLATE = 'mytemplate'; // Switch here- ✅ User sees pre-loaded sample XML on first visit
- ✅ User can edit XML in left panel with syntax highlighting
- ✅ Preview updates in real-time (< 500ms delay)
- ✅ Invalid XML shows error message
- ✅ HTML export downloads correctly with embedded CSS
- ✅ Developer can switch templates by changing config constant
- ✅ Application works in Chrome, Firefox, Safari
- ✅ Responsive layout works on 1024px+ screens
- ✅ No critical accessibility violations
- Zero console errors in production
- Lighthouse score: Performance > 90, Accessibility > 95
- All core features tested and working
- Developer documentation for adding templates complete
Document Version: 3.0 Last Updated: 2025-10-11 Author: Product Team Status: MVP Plan Finalized → Ready for MVP 1 Implementation