legal-markdown-js
Version:
Node.js implementation of LegalMarkdown for processing legal documents with markdown and YAML - Complete feature parity with Ruby version
2,424 lines ⢠83.3 kB
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Legal Markdown JS - Playground</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="app-container">
<header class="app-header">
<h1>Legal Markdown JS - Playground</h1>
<div class="header-actions">
<a href="https://github.com/petalo/legal-markdown-js" target="_blank" rel="noopener noreferrer" class="repo-link" title="View project repository">
š Repository
</a>
<button class="theme-toggle" title="Toggle theme">š</button>
<button class="help-btn" title="Help">ā</button>
</div>
</header>
<main class="editor-container">
<div class="input-panel">
<div class="panel-header">
<h3>Input</h3>
<div class="panel-actions">
<label class="file-input-label">
š Upload File
<input type="file" class="file-input" accept=".md,.txt" style="display: none;">
</label>
<div class="example-dropdown">
<select class="example-select">
<option value="">š Examples</option>
<option value="service-agreement">Service Agreement</option>
<option value="lease-contract">Lease Contract</option>
<option value="purchase-ticket">Purchase Ticket</option>
<option value="nda">Non-Disclosure Agreement</option>
<option value="employment-contract">Employment Contract</option>
<option value="features-demo">šÆ Features Demo</option>
</select>
</div>
</div>
</div>
<textarea class="editor" placeholder="Enter your Legal Markdown content here...
Select an example from the dropdown above to get started, or write your own content using YAML front matter and Legal Markdown syntax.
Example structure:
---
title: Document Title
variable: value
---
# {{title}}
Your content with {{variable}} substitutions..."></textarea>
</div>
<div class="column-resizer" id="resizer1"></div>
<!-- CSS Editor Panel -->
<div class="css-panel">
<div class="panel-header">
<h3>Custom CSS</h3>
<div class="panel-actions">
<label class="file-input-label">
š Upload CSS
<input type="file" class="css-file-input" accept=".css" style="display: none;">
</label>
<div class="example-dropdown">
<select class="css-example-select">
<option value="">šØ CSS Examples</option>
<option value="default">Default Legal Styles</option>
<option value="modern">Modern Style</option>
<option value="classic">Classic Legal</option>
<option value="minimal">Minimal</option>
</select>
</div>
<button class="css-reset secondary">š</button>
</div>
</div>
<div class="css-content">
<textarea class="css-editor" placeholder="Enter your custom CSS here...
/* Example: Change document colors */
body {
background: #f9f9f9;
color: #333;
}
h1 {
color: #2c3e50;
border-bottom: 2px solid #3498db;
}
/* Example: Customize spacing */
p {
margin-bottom: 1.2em;
line-height: 1.6;
}
/* Your custom styles here... */"></textarea>
</div>
</div>
<div class="column-resizer" id="resizer2"></div>
<div class="output-panel">
<div class="panel-header">
<h3>Preview</h3>
<div class="panel-actions">
<button class="toggle-styles-btn" title="Toggle base styles">šØ</button>
<button class="process-btn">ā¶ļø</button>
<button class="download-btn secondary">š¾</button>
<button class="download-pdf-btn secondary">š</button>
<button class="print-btn secondary">šØļø</button>
</div>
</div>
<div class="preview-container">
<div class="preview-split">
<div class="preview-block markdown-preview">
<div class="preview-header">
<h4>Markdown Output</h4>
<button class="copy-btn" data-target="markdown" title="Copy markdown">š</button>
</div>
<div class="preview-content">
<div class="preview-placeholder">
<p>š Enter your Legal Markdown content and click Process to see the result</p>
</div>
</div>
</div>
<div class="preview-resize-handle"></div>
<div class="preview-block html-preview">
<div class="preview-header">
<h4>HTML Preview</h4>
<button class="copy-btn" data-target="html" title="Copy HTML">š</button>
</div>
<div class="preview-content">
<div class="preview-placeholder">
<p>š Enter your Legal Markdown content and click Process to see the result</p>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<footer class="options-panel">
<button class="options-toggle">š½ Processing Options</button>
<div class="options-content open">
<div class="options-grid">
<div class="option-group">
<label>Processing Control</label>
<div class="checkbox-group">
<label class="checkbox-item">
<input type="checkbox" id="debug"> Debug mode
<span class="tooltip">
<span class="tooltip-icon">ā¹ļø</span>
<span class="tooltip-text">Shows detailed processing information in the console, including step-by-step parsing and variable substitution details.</span>
</span>
</label>
<label class="checkbox-item">
<input type="checkbox" id="yamlOnly"> Skip YAML
<span class="tooltip">
<span class="tooltip-icon">ā¹ļø</span>
<span class="tooltip-text">Skip YAML variable substitution in content. Returns raw markdown with variables unprocessed (e.g., {{title}} stays as {{title}}). Useful for testing content without variable replacement.</span>
</span>
</label>
<label class="checkbox-item">
<input type="checkbox" id="noHeaders"> No headers
<span class="tooltip">
<span class="tooltip-icon">ā¹ļø</span>
<span class="tooltip-text">Disables automatic header numbering (l., ll., lll.) that's typically used in legal documents.</span>
</span>
</label>
<label class="checkbox-item">
<input type="checkbox" id="enableFieldTracking" checked> Field tracking
<span class="tooltip">
<span class="tooltip-icon">ā¹ļø</span>
<span class="tooltip-text">Adds HTML spans around template variables and headers for debugging and styling. When disabled, maintains compatibility with Ruby version.</span>
</span>
</label>
</div>
</div>
<div class="option-group">
<label>Advanced Options</label>
<div class="checkbox-group">
<label class="checkbox-item">
<input type="checkbox" id="noClauses"> No clauses
<span class="tooltip">
<span class="tooltip-icon">ā¹ļø</span>
<span class="tooltip-text">Disables processing of structured clauses and conditional blocks (e.g., [text]{condition}). Content will be processed as plain text.</span>
</span>
</label>
<label class="checkbox-item">
<input type="checkbox" id="noReferences"> No references
<span class="tooltip">
<span class="tooltip-icon">ā¹ļø</span>
<span class="tooltip-text">Disables processing of cross-references and citation links within the document. References will appear as plain text.</span>
</span>
</label>
<label class="checkbox-item">
<input type="checkbox" id="noMixins"> No mixins
<span class="tooltip">
<span class="tooltip-icon">ā¹ļø</span>
<span class="tooltip-text">Disables processing of mixins and helper functions (e.g., {{formatDate()}}, {{capitalize()}}). Only basic variable substitution will work.</span>
</span>
</label>
<label class="checkbox-item">
<input type="checkbox" id="noImports"> No imports
<span class="tooltip">
<span class="tooltip-icon">ā¹ļø</span>
<span class="tooltip-text">Disables importing of external markdown files. Import statements will be ignored and appear as plain text.</span>
</span>
</label>
</div>
</div>
<div class="option-group">
<label>Header Options</label>
<div class="checkbox-group">
<label class="checkbox-item">
<input type="checkbox" id="noReset"> No reset
<span class="tooltip">
<span class="tooltip-icon">ā¹ļø</span>
<span class="tooltip-text">Prevents resetting header counters. Header numbering will continue from previous values instead of starting fresh.</span>
</span>
</label>
<label class="checkbox-item">
<input type="checkbox" id="noIndent"> No indent
<span class="tooltip">
<span class="tooltip-icon">ā¹ļø</span>
<span class="tooltip-text">Disables automatic indentation of nested headers and sub-clauses. All content will be left-aligned.</span>
</span>
</label>
<label class="checkbox-item">
<input type="checkbox" id="throwOnYamlError"> Throw on YAML error
<span class="tooltip">
<span class="tooltip-icon">ā¹ļø</span>
<span class="tooltip-text">Stops processing and shows an error if YAML front matter is malformed. Otherwise, continues with partial parsing.</span>
</span>
</label>
</div>
</div>
<div class="option-group">
<label>Export Options</label>
<div class="checkbox-group">
<label class="checkbox-item">
<input type="checkbox" id="exportYaml"> Export YAML
<span class="tooltip">
<span class="tooltip-icon">ā¹ļø</span>
<span class="tooltip-text">Exports document metadata as a YAML file alongside the processed markdown. Useful for data exchange and backup.</span>
</span>
</label>
<label class="checkbox-item">
<input type="checkbox" id="exportJson"> Export JSON
<span class="tooltip">
<span class="tooltip-icon">ā¹ļø</span>
<span class="tooltip-text">Exports document metadata as a JSON file alongside the processed markdown. Useful for integration with other systems.</span>
</span>
</label>
</div>
<input type="text" id="title" placeholder="Document title (optional)">
</div>
</div>
</div>
</footer>
<div id="messageArea"></div>
</div>
<script type="module">
// Import the modern ES module bundle
import LegalMarkdown from './legal-markdown-browser.js';
// Make it available globally for the playground code
window.LegalMarkdown = LegalMarkdown;
// Simple verification that the library loaded correctly
if (typeof window.LegalMarkdown === 'undefined') {
console.error('Legal Markdown library not loaded');
} else {
console.log('ā
Legal Markdown library loaded successfully');
console.log('Available functions:', Object.keys(window.LegalMarkdown));
}
</script>
<script>
class LegalMarkdownApp {
constructor() {
this.processedResult = null;
this.metadataResult = null;
this.theme = localStorage.getItem('theme') || 'light';
this.customCSS = '';
this.baseStylesEnabled = true; // Track base styles state
this.initializeTheme();
this.initializeElements();
this.bindEvents();
this.initializePreviewResizer();
this.updateBaseStyles(); // Initialize base styles
this.loadExample('service-agreement');
this.loadCSSExample('default'); // Load default CSS by default
}
initializeTheme() {
document.documentElement.setAttribute('data-theme', this.theme);
const themeToggle = document.querySelector('.theme-toggle');
if (themeToggle) {
themeToggle.textContent = this.theme === 'dark' ? 'āļø' : 'š';
}
}
initializeElements() {
this.editor = document.querySelector('.editor');
this.preview = document.querySelector('.preview-container');
this.markdownPreview = document.querySelector('.markdown-preview .preview-content');
this.htmlPreview = document.querySelector('.html-preview .preview-content');
this.processBtn = document.querySelector('.process-btn');
this.toggleStylesBtn = document.querySelector('.toggle-styles-btn');
this.downloadBtn = document.querySelector('.download-btn');
this.downloadPdfBtn = document.querySelector('.download-pdf-btn');
this.printBtn = document.querySelector('.print-btn');
this.themeToggle = document.querySelector('.theme-toggle');
this.optionsToggle = document.querySelector('.options-toggle');
this.optionsContent = document.querySelector('.options-content');
this.fileInput = document.querySelector('.file-input');
this.exampleSelect = document.querySelector('.example-select');
this.messageArea = document.getElementById('messageArea');
// CSS Editor elements
this.cssReset = document.querySelector('.css-reset');
this.cssContent = document.querySelector('.css-content');
this.cssEditor = document.querySelector('.css-editor');
this.cssFileInput = document.querySelector('.css-file-input');
this.cssExampleSelect = document.querySelector('.css-example-select');
// Column resizers
this.resizer1 = document.getElementById('resizer1');
this.resizer2 = document.getElementById('resizer2');
this.previewResizer = document.querySelector('.preview-resize-handle');
}
bindEvents() {
this.processBtn.addEventListener('click', () => this.processContent());
this.toggleStylesBtn.addEventListener('click', () => this.toggleBaseStyles());
this.downloadBtn.addEventListener('click', () => this.downloadDocument());
this.downloadPdfBtn.addEventListener('click', () => this.downloadPDF());
this.printBtn.addEventListener('click', () => this.printDocument());
this.themeToggle.addEventListener('click', () => this.toggleTheme());
this.optionsToggle.addEventListener('click', () => this.toggleOptions());
this.fileInput.addEventListener('change', (e) => this.handleFileUpload(e));
this.exampleSelect.addEventListener('change', (e) => this.loadExample(e.target.value));
// CSS Editor events
this.cssReset.addEventListener('click', () => this.resetCSS());
this.cssFileInput.addEventListener('change', (e) => this.handleCSSFileUpload(e));
this.cssExampleSelect.addEventListener('change', (e) => this.loadCSSExample(e.target.value));
// Auto-apply CSS on input (debounced)
this.cssEditor.addEventListener('input',
this.debounce(() => this.applyCSS(), 500)
);
// Column resizing
this.initColumnResizing();
// Help button
const helpBtn = document.querySelector('.help-btn');
if (helpBtn) {
helpBtn.addEventListener('click', () => this.showHelp());
}
// Copy buttons
const copyBtns = document.querySelectorAll('.copy-btn');
copyBtns.forEach(btn => {
btn.addEventListener('click', (e) => this.copyContent(e.target.dataset.target));
});
// Auto-process on input (debounced)
this.editor.addEventListener('input',
this.debounce(() => this.processContent(), 1000)
);
// Keyboard shortcuts
document.addEventListener('keydown', (e) => this.handleKeyboardShortcuts(e));
}
async processContent() {
const content = this.editor.value;
if (!content.trim()) {
this.showMessage('Please enter some content to process.', 'error');
this.clearPreview();
return;
}
// Validate content structure
const validation = this.validateContent(content);
if (!validation.isValid) {
this.showMessage(validation.message, 'warning');
if (validation.severity === 'error') {
this.clearPreview();
return;
}
}
try {
this.showProcessing();
const options = this.getProcessingOptions();
const result = await window.LegalMarkdown.processLegalMarkdownWithRemark(content, options);
this.processedResult = result.content;
this.metadataResult = result.metadata;
this.displayResult(result);
this.showProcessingStats(result);
} catch (error) {
console.error('Processing error:', error);
this.handleProcessingError(error);
} finally {
this.hideProcessing();
}
}
getProcessingOptions() {
return {
debug: document.getElementById('debug').checked,
yamlOnly: document.getElementById('yamlOnly').checked,
noHeaders: document.getElementById('noHeaders').checked,
noClauses: document.getElementById('noClauses').checked,
noReferences: document.getElementById('noReferences').checked,
noMixins: document.getElementById('noMixins').checked,
noImports: document.getElementById('noImports').checked,
noReset: document.getElementById('noReset').checked,
noIndent: document.getElementById('noIndent').checked,
throwOnYamlError: document.getElementById('throwOnYamlError').checked,
enableFieldTracking: document.getElementById('enableFieldTracking').checked,
enableFieldTrackingInMarkdown: document.getElementById('enableFieldTracking').checked,
exportMetadata: document.getElementById('exportYaml').checked || document.getElementById('exportJson').checked,
exportFormat: document.getElementById('exportYaml').checked ? 'yaml' : 'json'
};
}
displayResult(result) {
// Display raw markdown output
this.markdownPreview.innerHTML = `
<div class="legal-document">
<pre style="white-space: pre-wrap; font-family: var(--font-mono); font-size: 13px; line-height: 1.4; margin: 0; padding: 0;">${result.content}</pre>
</div>
`;
// Display HTML-formatted output
this.htmlPreview.innerHTML = `
<div class="legal-document">
${this.formatMarkdownForDisplay(result.content)}
</div>
`;
// Apply base styles if enabled
this.updateBaseStyles();
// Apply custom CSS if present
this.updatePreviewWithCSS();
if (result.fieldReport) {
console.log('Field Report:', result.fieldReport);
}
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
copyContent(target) {
if (!this.processedResult) {
this.showMessage('No content to copy. Please process a document first.', 'error');
return;
}
let textToCopy = '';
if (target === 'markdown') {
textToCopy = this.processedResult;
} else if (target === 'html') {
textToCopy = this.formatMarkdownForDisplay(this.processedResult);
}
if (textToCopy) {
navigator.clipboard.writeText(textToCopy).then(() => {
this.showMessage(`${target.toUpperCase()} copied to clipboard!`, 'success');
}).catch(err => {
this.showMessage(`Failed to copy ${target}`, 'error');
console.error('Copy failed:', err);
});
}
}
formatMarkdownForDisplay(content) {
// Simple markdown to HTML conversion without external dependencies
// This is a basic implementation focused on legal document structure
try {
let html = content
// Headers
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
// Bold and italic
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>')
// Links
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>')
// Line breaks
.replace(/\n\n/g, '</p><p>')
.replace(/\n/g, '<br>')
// Lists
.replace(/^\- (.*$)/gim, '<li>$1</li>')
.replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>')
// Code blocks
.replace(/```([^`]*)```/g, '<pre><code>$1</code></pre>')
.replace(/`([^`]*)`/g, '<code>$1</code>');
// Wrap paragraphs
if (!html.startsWith('<')) {
html = '<p>' + html + '</p>';
}
return html;
} catch (error) {
console.error('Simple markdown parsing failed:', error);
return `<pre>${this.escapeHtml(content)}</pre>`;
}
}
downloadDocument() {
if (!this.processedResult) {
this.showMessage('Please process a document first.', 'error');
return;
}
const title = document.getElementById('title').value || 'legal-document';
const filename = `${title.replace(/[^a-zA-Z0-9]/g, '-')}.md`;
this.downloadFile(this.processedResult, filename);
}
printDocument() {
if (!this.processedResult) {
this.showMessage('Please process a document first.', 'error');
return;
}
const printContent = this.preview.innerHTML;
const title = document.getElementById('title').value || 'Legal Document';
const printWindow = window.open('', '_blank');
printWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<title>${title}</title>
<style>
${this.getContractCSS()}
</style>
</head>
<body onload="window.print(); window.close();">
${printContent}
</body>
</html>
`);
printWindow.document.close();
}
async downloadPDF() {
if (!this.processedResult) {
this.showMessage('Please process a document first.', 'error');
return;
}
const title = document.getElementById('title').value || 'legal-document';
const printContent = this.preview.innerHTML;
try {
// Show loading message
this.showMessage('Generating PDF... Please wait.', 'info');
// Create a hidden iframe for PDF generation
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
document.body.appendChild(iframe);
iframe.contentDocument.open();
iframe.contentDocument.write(`
<!DOCTYPE html>
<html>
<head>
<title>${title}</title>
<style>
${this.getContractCSS()}
@media print {
body { margin: 0; }
@page { margin: 15mm; }
}
</style>
</head>
<body>
${printContent}
</body>
</html>
`);
iframe.contentDocument.close();
// Wait for content to load
await new Promise(resolve => setTimeout(resolve, 500));
// Focus the iframe and trigger print
iframe.contentWindow.focus();
iframe.contentWindow.print();
// Clean up
setTimeout(() => {
document.body.removeChild(iframe);
this.showMessage('PDF generation initiated. Use your browser\'s print dialog to save as PDF.', 'success');
}, 1000);
} catch (error) {
console.error('PDF generation error:', error);
this.showMessage('PDF generation failed. Please use the Print button instead.', 'error');
}
}
getContractCSS() {
const baseCSS = `
body {
font-family: Helvetica, Arial, sans-serif;
font-size: 12pt;
line-height: 1.3;
color: #333;
max-width: 210mm;
margin: 0 auto;
padding: 20mm 15mm;
background-color: white;
}
h1 {
color: #353954;
font-size: 1.6em;
line-height: 1.5em;
margin: 8px 8px 32px;
text-align: center;
border-bottom: solid #02370a;
padding: 0 5% 0.3em;
}
h2 {
font-size: 14pt;
margin-top: 20px;
margin-bottom: 15px;
border-bottom: 1px solid #41badf;
}
h3 {
font-size: 10pt;
font-weight: bold;
border-bottom: 1px solid #41badf;
margin-top: 30px;
margin-bottom: 8px;
}
p {
margin-bottom: 15px;
text-align: justify;
}
@media print {
@page {
size: A4;
margin: 20mm 10mm 10mm 10mm;
}
body {
font-size: 11pt;
color: black;
}
}
`;
// Add custom CSS if present
if (this.customCSS.trim()) {
return baseCSS + '\n\n/* Custom CSS */\n' + this.customCSS;
}
return baseCSS;
}
toggleTheme() {
this.theme = this.theme === 'light' ? 'dark' : 'light';
localStorage.setItem('theme', this.theme);
this.initializeTheme();
}
toggleOptions() {
this.optionsContent.classList.toggle('open');
this.optionsToggle.textContent = this.optionsContent.classList.contains('open') ?
'š½ Processing Options' :
'āļø Processing Options';
}
handleFileUpload(event) {
const file = event.target.files[0];
if (!file) return;
// Validate file type
const allowedTypes = ['text/plain', 'text/markdown', ''];
const allowedExtensions = ['.md', '.txt', '.markdown'];
const fileExtension = file.name.toLowerCase().substr(file.name.lastIndexOf('.'));
if (!allowedTypes.includes(file.type) && !allowedExtensions.includes(fileExtension)) {
this.showMessage('Please select a valid text file (.md, .txt, .markdown)', 'error');
return;
}
// Validate file size (max 1MB)
const maxSize = 1024 * 1024; // 1MB
if (file.size > maxSize) {
this.showMessage('File too large. Maximum size is 1MB.', 'error');
return;
}
const reader = new FileReader();
reader.onload = (e) => {
try {
const content = e.target.result;
this.editor.value = content;
this.exampleSelect.value = ''; // Clear example selection
this.processContent();
this.showMessage(`š File "${file.name}" loaded successfully`, 'success');
} catch (error) {
this.showMessage('Error reading file: ' + error.message, 'error');
}
};
reader.onerror = () => {
this.showMessage('Error reading file. Please try again.', 'error');
};
reader.readAsText(file);
}
getExamples() {
return {
'service-agreement': {
title: 'Service Agreement',
content: `---
title: Service Agreement
client: Acme Corporation
provider: Professional Services LLC
effective_date: 2024-01-01
payment_terms: 30
jurisdiction: California
---
# {{title}}
This {{title}} is entered into on {{formatDate(effective_date, "YYYY-MM-DD")}} between:
**CLIENT**: {{client}}
**PROVIDER**: {{provider}}
l. Terms and Conditions
ll. Services
The Provider agrees to perform professional services as detailed in the attached statement of work.
ll. Payment Terms
Payment is due within {{payment_terms}} days of invoice date.
ll. Jurisdiction
This Agreement shall be governed by the laws of {{jurisdiction}}.
l. Conclusion
This concludes the {{title}} between the parties.`
},
'lease-contract': {
title: 'Office Lease Agreement',
content: `---
title: OFFICE SPACE LEASE AGREEMENT
contract:
signing_date: 2024-01-15
signing_city: San Francisco
lessor:
company_name: Property Management LLC
registered_address: 123 Main Street, San Francisco, CA 94105
tax_id: 12-3456789
representative:
full_name: John Smith
id_number: 123456789
lessee:
company_name: Tech Startup Inc
registered_address: 456 Innovation Way, San Francisco, CA 94107
tax_id: 98-7654321
representative:
full_name: Jane Doe
id_number: ""
property:
address: 789 Business Plaza, Suite 500, San Francisco, CA 94108
area_sqm: 250
parking_spots: 5
payment:
monthly_rent:
text: Five Thousand
number: 5000
late_fee_applies: true
late_fee_percentage: 1.5
maintenance_included: true
maintenance:
included_services:
- Daily cleaning
- HVAC maintenance
- Security services
---
# {{title}}
This agreement is entered into on {{contract.signing_date}} in {{contract.signing_city}}.
## Parties
Between:
**{{lessor.company_name}}**, with registered office at {{lessor.registered_address}}, and Tax ID {{lessor.tax_id}}, hereinafter referred to as "THE LESSOR".
Represented by {{lessor.representative.full_name}} with ID {{lessor.representative.id_number}}.
And:
**{{lessee.company_name}}**, with registered office at {{lessee.registered_address}}, and Tax ID {{lessee.tax_id}}, hereinafter referred to as "THE LESSEE".
Represented by {{lessee.representative.full_name}} with ID {{lessee.representative.id_number ? lessee.representative.id_number : "[ID Required]"}}.
## Property Details
- **Address**: {{property.address}}
- **Area**: {{property.area_sqm}} square meters
- **Parking Spots**: {{property.parking_spots}}
- **Monthly Rent**: {{payment.monthly_rent.text}} ({{payment.monthly_rent.number}}) USD
[## Late Payment Clause
Late payments will incur a fee of {{payment.late_fee_percentage}}% per month.]{payment.late_fee_applies}
[## Maintenance
The following maintenance services are included:
{{#maintenance_included}}
{{#each maintenance.included_services}}
- {{this}}
{{/each}}
{{/maintenance_included}}]{maintenance_included}`
},
'purchase-ticket': {
title: 'Purchase Ticket',
content: '---\n' +
'storeName: TechMart Store\n' +
'cashierName: Alice Johnson\n' +
'ticketNumber: "12345"\n' +
'receiptId: RCP-2024-12345\n' +
'taxRate: 8.5\n' +
'items:\n' +
' - name: Laptop Computer\n' +
' price: 999.99\n' +
' onSale: false\n' +
' - name: Wireless Mouse\n' +
' price: 29.99\n' +
' onSale: true\n' +
' - name: USB Cable\n' +
' price: null\n' +
'subtotal: 1029.98\n' +
'taxAmount: 87.55\n' +
'total: 1117.53\n' +
'loyaltyMember: true\n' +
'pointsEarned: 112\n' +
'pointsBalance: 1523\n' +
'---\n' +
'\n' +
'# Purchase Ticket - {{ticketNumber}}\n' +
'\n' +
'**Date:** @today **Store:** {{storeName}}\n' +
'**Cashier:** {{cashierName}}\n' +
'\n' +
'## Purchased Items\n' +
'\n' +
'{{#items}}\n' +
'- {{name}} {{price ? "$" + price : "[Price Missing]"}} {{onSale ? "(ON SALE!)" : ""}}\n' +
'{{/items}}\n' +
'\n' +
'## Payment Details\n' +
'\n' +
'- Subtotal: ${{subtotal}}\n' +
'- Tax ({{taxRate}}%): ${{taxAmount}}\n' +
'- **Total**: ${{total}}\n' +
'\n' +
'{{#loyaltyMember}}\n' +
'## Customer Loyalty\n' +
'\n' +
'- Member Points Earned: {{pointsEarned}}\n' +
'- Current Balance: {{pointsBalance}}\n' +
'{{/loyaltyMember}}\n' +
'\n' +
'---\n' +
'\n' +
'Thank you for shopping with us!\n' +
'Receipt ID: {{receiptId}}'
},
'nda': {
title: 'Non-Disclosure Agreement',
content: `---
title: NON-DISCLOSURE AGREEMENT
disclosing_party: TechCorp Inc.
receiving_party: John Developer
effective_date: 2024-01-01
duration_months: 24
jurisdiction: Delaware
---
# {{title}}
This {{title}} is entered into on {{effective_date}} between:
**DISCLOSING PARTY**: {{disclosing_party}}
**RECEIVING PARTY**: {{receiving_party}}
l. Definition of Confidential Information
ll. Confidential Information includes all technical data, trade secrets, know-how, research, product plans, products, services, customers, customer lists, markets, software, developments, inventions, processes, formulas, technology, designs, drawings, engineering, hardware configuration information, marketing, finances, or other business information.
l. Obligations of Receiving Party
ll. Protection of Confidential Information
The Receiving Party agrees to hold and maintain the Confidential Information in strict confidence.
ll. Non-Disclosure
The Receiving Party agrees not to disclose any Confidential Information to third parties without prior written consent.
ll. Use Restriction
The Receiving Party agrees to use the Confidential Information solely for the purpose of evaluating potential business opportunities.
l. Term
This Agreement shall remain in effect for {{duration_months}} months from the effective date.
l. Governing Law
This Agreement shall be governed by the laws of {{jurisdiction}}.
l. Signatures
By signing below, both parties agree to the terms and conditions of this Agreement.
**{{disclosing_party}}**
Signature: ___________________________ Date: ___________
**{{receiving_party}}**
Signature: ___________________________ Date: ___________`
},
'employment-contract': {
title: 'Employment Contract',
content: '---\n' +
'title: EMPLOYMENT AGREEMENT\n' +
'company: TechCorp Inc.\n' +
'employee: Jane Smith\n' +
'position: Senior Software Engineer\n' +
'start_date: 2024-02-01\n' +
'salary: 95000\n' +
'benefits_package: Standard\n' +
'probation_period: 3\n' +
'vacation_days: 20\n' +
'---\n' +
'\n' +
'# {{title}}\n' +
'\n' +
'This {{title}} is entered into between:\n' +
'\n' +
'**COMPANY**: {{company}}\n' +
'**EMPLOYEE**: {{employee}}\n' +
'\n' +
'l. Position and Duties\n' +
'll. Position\n' +
'The Employee is hereby employed as {{position}}.\n' +
'\n' +
'll. Start Date\n' +
'Employment shall commence on {{start_date}}.\n' +
'\n' +
'll. Probationary Period\n' +
'The Employee shall serve a probationary period of {{probation_period}} months.\n' +
'\n' +
'l. Compensation and Benefits\n' +
'll. Salary\n' +
'The Employee shall receive an annual salary of ${{salary}}.\n' +
'\n' +
'll. Benefits\n' +
'The Employee shall be entitled to the {{benefits_package}} benefits package.\n' +
'\n' +
'll. Vacation\n' +
'The Employee shall be entitled to {{vacation_days}} vacation days per year.\n' +
'\n' +
'l. Termination\n' +
'll. Either party may terminate this agreement with 30 days written notice.\n' +
'\n' +
'l. Confidentiality\n' +
'll. The Employee agrees to maintain the confidentiality of all company information.\n' +
'\n' +
'l. Governing Law\n' +
'This Agreement shall be governed by the laws of the state in which the Company is located.\n' +
'\n' +
'**Signatures**\n' +
'\n' +
'Company Representative: ___________________________ Date: ___________\n' +
'\n' +
'Employee: ___________________________ Date: ___________'
},
'features-demo': {
title: 'Legal Markdown Features Demo',
content: '---\n' +
'# This is a comprehensive playground document to demonstrate all Legal Markdown JS features\n' +
'title: Legal Markdown JS - Features Playground\n' +
'document_type: Interactive Demo\n' +
'doc_version: v2.0\n' +
'\n' +
'# Basic Document Information\n' +
'client_name: john doe\n' +
'company_name: acme corporation\n' +
'project_name: legal markdown integration\n' +
'contract_amount: 50000\n' +
'currency: USD\n' +
'payment_terms: 30\n' +
'jurisdiction: California\n' +
'\n' +
'# Date Variables\n' +
'start_date: 2024-01-15\n' +
'end_date: 2024-12-31\n' +
'project_duration: 12\n' +
'\n' +
'# Nested Objects\n' +
'client:\n' +
' company:\n' +
' name: Acme Corporation\n' +
' address:\n' +
' street: 123 Business St\n' +
' city: San Francisco\n' +
' state: CA\n' +
' zip: 94105\n' +
' contact:\n' +
' name: John Smith\n' +
' email: john.smith@acme.com\n' +
' phone: (555) 123-4567\n' +
'\n' +
'# Arrays for Loops\n' +
'services:\n' +
' - name: Web Development\n' +
' hours: 120\n' +
' rate: 150\n' +
' description: Frontend and backend development\n' +
' - name: UI/UX Design\n' +
' hours: 60\n' +
' rate: 120\n' +
' description: User interface and experience design\n' +
' - name: Project Management\n' +
' hours: 40\n' +
' rate: 100\n' +
' description: Project coordination and management\n' +
'\n' +
'team_members:\n' +
' - name: Alice Johnson\n' +
' role: Senior Developer\n' +
' - name: Bob Wilson\n' +
' role: Designer\n' +
' - name: Carol Davis\n' +
' role: Project Manager\n' +
'\n' +
'# Boolean Variables\n' +
'includes_warranty: true\n' +
'warranty_period: 12\n' +
'premium_support: true\n' +
'confidentiality_required: true\n' +
'international_project: false\n' +
'\n' +
'# Numerical Variables\n' +
'tax_rate: 0.21\n' +
'discount_rate: 0.15\n' +
'population: 1234567\n' +
'sequence_number: 42\n' +
'---\n' +
'\n' +
'# {{title}}\n' +
'\n' +
'_This document demonstrates Legal Markdown JS features in the web playground._\n' +
'\n' +
'**Document Type:** {{document_type}}\n' +
'**Version:** {{doc_version}}\n' +
'**Generated:** @today\n' +
'\n' +
'---\n' +
'\n' +
'l. Basic Variable Substitution\n' +
'\n' +
'll. Simple Variables\n' +
'Client: {{client_name}}\n' +
'Company: {{company_name}}\n' +
'Project: {{project_name}}\n' +
'Amount: ${{contract_amount}}\n' +
'Currency: {{currency}}\n' +
'Payment Terms: {{payment_terms}} days\n' +
'Jurisdiction: {{jurisdiction}}\n' +
'\n' +
'll. Nested Object Access\n' +
'Company Name: {{client.company.name}}\n' +
'Address: {{client.company.address.street}}, {{client.company.address.city}}, {{client.company.address.state}} {{client.company.address.zip}}\n' +
'Contact: {{client.contact.name}} ({{client.contact.email}})\n' +
'Phone: {{client.contact.phone}}\n' +
'\n' +
'll. Array Access\n' +
'First Service: {{services.0.name}} - {{services.0.hours}} hours at ${{services.0.rate}}/hour\n' +
'Second Service: {{services.1.name}} - {{services.1.hours}} hours at ${{services.1.rate}}/hour\n' +
'Third Service: {{services.2.name}} - {{services.2.hours}} hours at ${{services.2.rate}}/hour\n' +
'Team Lead: {{team_members.0.name}} ({{team_members.0.role}})\n' +
'\n' +
'l. String Helpers\n' +
'\n' +
'll. Case Conversion\n' +
'Original: {{client_name}}\n' +
'Capitalized: {{capitalize(client_name)}}\n' +
'Title Case: {{titleCase(client_name)}}\n' +
'Uppercase: {{upper(client_name)}}\n' +
'Lowercase: {{lower(client_name)}}\n' +
'Words Capitalized: {{capitalizeWords(client_name)}}\n' +
'Test Simple: {{upper("test")}}\n' +
'\n' +
'll. String Manipulation\n' +
'Initials: {{initials(client_name)}}\n' +
'Truncated Project: {{truncate(project_name, 15, "...")}}\n' +
'Replaced Spaces: {{replaceAll(client_name, " ", "_")}}\n' +
'Cleaned Text: {{clean(" extra spaces ")}}\n' +
'\n' +
'll. Case Conversions\n' +
'camelCase: {{camelCase(client_name)}}\n' +
'PascalCase: {{pascalCase(client_name)}}\n' +
'kebab-case: {{kebabCase(client_name)}}\n' +
'snake_case: {{snakeCase(client_name)}}\n' +
'\n' +
'l. Date Processing\n' +
'\n' +
'll. Today Date Processing\n' +
'Today (default): @today\n' +
'Today (ISO): @today[iso]\n' +
'Today (long): @today[long]\n' +
'Today (European): @today[european]\n' +
'Today (legal): @today[legal]\n' +
'\n' +
'll. Date Helpers with Variables\n' +
'Start Date: {{formatDate(start_date, "MMMM Do, YYYY")}}\n' +
'End Date: {{formatDate(end_date, "MMMM Do, YYYY")}}\n' +
'Contract Duration: {{project_duration}} months\n' +
'\n' +
'll. Date Calculations with Date Variables\n' +
'Future Date (1 year): {{formatDate(addYears(start_date, 1), "YYYY-MM-DD")}}\n' +
'Future Date (6 months): {{formatDate(addMonths(start_date, 6), "MMMM YYYY")}}\n' +
'Future Date (30 days): {{formatDate(addDays(start_date, 30), "DD/MM/YYYY")}}\n' +
'Payment Due: {{formatDate(addDays(start_date, payment_terms), "MMMM Do, YYYY")}}\n' +
'\n' +
'l. Number Helpers\n' +
'\n' +
'll. Currency Formatting\n' +
'Dollar Format: {{formatDollar(contract_amount)}}\n' +
'Euro Format: {{formatEuro(contract_amount)}}\n' +
'Pound Format: {{formatPound(contract_amount)}}\n' +
'Currency USD: {{formatCurrency(contract_amount, "USD")}}\n' +
'Currency EUR: {{formatCurrency(contract_amount, "EUR")}}\n' +
'Currency GBP: {{formatCurrency(contract_amount, "GBP")}}\n' +
'\n' +
'll. Number Formatting\n' +
'Population: {{formatInteger(population)}}\n' +
'Tax Rate: {{formatPercent(tax_rate, 1)}}\n' +
'Discount: {{formatPercent(discount_rate, 2)}}\n' +
'Rounded Tax: {{round(tax_rate, 2)}}\n' +
'\n' +
'll. Numbers to Words\n' +
'Sequence: {{numberToWords(sequence_number)}}\n' +
'Amount: {{capitalize(numberToWords(contract_amount))}}\n' +
'\n' +
'l. Conditional Content\n' +
'\n' +
'll. Basic Conditionals\n' +
'[This project includes warranty coverage.]{includes_warranty}\n' +
'[Premium support is included in this package.]{premium_support}\n' +
'[This is a confidential project.]{confidentiality_required}\n' +
'[This is an international project.]{international_project}\n' +
'\n' +
'll. Conditional Expressions\n' +
'Support Level: {{premium_support ? "Premium Support Included" : "Standard Support"}}\n' +
'Project Type: {{international_project ? "International Project" : "Domestic Project"}}\n' +
'Warranty Status: {{includes_warranty ? "Warranty Included" : "No Warranty"}}\n' +
'\n' +
'l. Template Loops\n' +
'\n' +
'll. Services List\n' +
'{{#services}}\n' +
'- **{{name}}**: {{description}}\n' +
' - Hours: {{hours}}\n' +
' - Rate: ${{rate}}/hour\n' +
' - Subtotal: ${{formatInteger(hours)}}\n' +
'{{/services}}\n' +
'\n' +
'll. Team Members\n' +
'{{#team_members}}\n' +
'- **{{name}}** ({{role}})\n' +
'{{/team_members}}\n' +
'\n' +
'll. Conditional Loop\n' +
'{{#includes_warranty}}\n' +
'## Warranty Information\n' +
'\n' +
'This project includes a {{warranty_period}}-month warranty period.\n' +
'Warranty expires: {{formatDate(addMonths(start_date, warranty_period), "MMMM Do, YYYY")}}\n' +
'{{/includes_warranty}}\n' +
'\n' +
'l. Advanced Features\n' +
'\n' +
'll. Nested Helpers\n' +
'Formatted Client: {{upper(titleCase(client_name))}}\n' +
'Contract ID: {{upper(initials(client_name))}}-@today[medium]\n' +
'Clean Company: {{titleCase(replaceAll(company_name, " ", "_"))}}\n' +
'Short Project: {{upper(truncate(project_name, 10, "..."))}}\n' +
'\n' +
'll. Simple Calculations\n' +
'Total Service Hours: {{services.0.hours}} + {{services.1.hours}} + {{services.2.hours}}\n' +
'First Service Value: {{formatDollar(18000)}}\n' +
'Second Service Value: {{formatDollar(7200)}}\n' +
'Third Service Value: {{formatDollar(4000)}}\n' +
'\n' +
'll. Pluralization\n' +
'Duration: {{project_duration}} {{pluralize("month", project_duration)}}\n' +
'Payment Terms: {{payment_terms}} {{pluralize("day", payment_terms)}}\n' +
'Warranty: {{warranty_period}} {{pluralize("month", warranty_period)}}\n' +
'\n' +
'l. Legal Document Structure\n' +
'\n' +
'll. Definitions\n' +
'For purposes of this Agreement:\n' +
'- "Client" means {{titleCase(client_name)}}\n' +
'- "Company" means {{client.company.name}}\n' +
'- "Project" means {{project_name}}\n' +
'- "Effective Date" means @today[long]\n' +
'\n' +
'll. Terms and Conditions\n' +
'This Agreement is effective as of @today[long] and shall remain in effect for {{project_duration}} months.\n' +
'\n' +
'lll. Payment Terms\n' +
'Payment shall be due within {{payment_terms}} {{pluralize("day", payment_terms)}} of invoice date.\n' +
'\n' +
'lll. Tax Information\n' +
'Applicable tax rate is {{formatPercent(tax_rate, 1)}} as determined by {{jurisdiction}} law.\n' +
'\n' +
'll. Governing Law\n' +
'This Agreement shall be governed by the laws of {{jurisdiction}}.\n' +
'\n' +
'l. Document Summary\n' +
'\n' +
'**Document Information:**\n' +
'- Generated: @today[legal]\n' +
'- Client: {{titleCase(client_name)}} ({{initials(client_name)}})\n' +
'- Project: {{project_name}}\n' +
'- Duration: {{project_duration}} {{pluralize("month", project_duration)}}\n' +
'- Total Value: {{formatDollar(contract_amount)}}\n' +
'- Payment Terms: {{payment_terms}} {{pluralize("day", payment_terms)}}\n' +
'- Tax Rate: {{formatPercent(tax_rate, 1)}}\n' +
'\n' +
'---\n' +
'\n' +
'_This document serves as a practical demonstration of Legal Markdown JS capabilities in the web playground._'
}
};
}
loadExample(exampleKey) {
if (!exampleKey) return;
const examples = this.getExamples();
const example = examples[exampleKey];
if (example) {
this.editor.value = example.content;
this.exampleSelect.value = exampleKey;
this.processContent();
} else {
this.showMessage('Example not found.', 'error');
}
}
validateContent(content) {
const validation = {
isValid: true,
message: '',
severity: 'info'
};
// Check for YAML front matter
const hasYamlFrontMatter = content.trim().startsWith('---');
if (!hasYamlFrontMatter) {
validation.isValid = false;
validation.message = 'No YAML front matter detected. Consider adding document metadata with ---';
validation.severity = 'warning';
return validation;
}
// Check for balanced YAML front matter
const yamlMatches = content.match(/^---\s*\n([\s\S]*?)\n---\s*$/m);
if (!yamlMatches) {
validation.isValid = false;
validation.message = 'YAML front matter is not properly formatted. Ensure it starts and ends with ---';
validation.severity = 'error';
return validation;
}
// Check for common YAML errors
const yamlContent = yamlMatches[1];
if (yamlContent.includes('\t')) {
validation.isValid = false;
validation.message = 'YAML contains tabs. Use spaces for indentation instead.';
validation.severity = 'error';
return validation;
}
// Check for template variables
const templateVars = content.match(/\{\{([^}]+)\}\}/g);
if (templateVars) {
const uniqueVars = [...new Set(templateVars.map(v => v.replace(/[{}]/g, '').trim()))];
if (uniqueVars.length > 10) {
validation.isValid = false;
validation.message = `Document contains ${uniqueVars.length} template variables. Consider breaking into sections.`;
validation.severity = 'warning';
}
}
// Check for unbalanced brackets
const openBrackets = (content.match(/\{\{/g) || []).length;
const closeBrackets = (content.match(/\}\}/g) || []).length;
if (openBrackets !== closeBrackets) {
validation.isValid = false;
validation.message = `Unbalanced template brackets: ${openBrackets} opening, ${closeBrackets} closing`;
validation.severity = 'error';
return validation;
}
return validation;
}
handleProcessingError(error) {
let userMessage = 'Error processing document: ';
let suggestions = [];
if (error.message.includes('YAML')) {
userMessage += 'YAML parsing error. ';
suggestions.push('Check your YAML syntax in the front matter');
suggestions.push('Ensure proper indentation with spaces, not tabs');
suggestions.push('Verify all YAML keys have values');
} else if (error.message.includes('template')) {
userMessage += 'Template processing error. ';
suggestions.push('Check that all variables are defined in YAML front matter');
suggestions.push('Verify template syntax: {{variable}}');
} else if (error.message.includes('reference')) {
userMessage += 'Reference processing error. ';
suggestions.push('Check reference syntax and file paths');
} else {
userMessage += error.message;
}
if (suggestions.length > 0) {
userMessage += '\n\nSuggestions:\n⢠' + suggestions.join('\n⢠');
}
this.showMessage(userMessage, 'error');
this.clearPreview();
}
showProcessingStats(result) {
let stats = [];
if (result.metadata) {
const yamlKeys = Object.keys(result.metadata).length;
stats.push(`${yamlKeys} YAML variables`);
}
if (result.fieldReport && Array.isArray(result.fieldReport)) {
stats.push(`${result.fieldReport.length} field substitutions`);
}
const wordCount = result.content.split(/\s+/).filter(word => word.length > 0).length;
stats.push(`${wordCount} words`);
if (stats.length > 0) {
this.showMessage(`ā
Processing complete: ${stats.join(', ')}`, 'success');
} else {
this.showMessage('Document processed successfully!', 'success');
}
}
clearPreview() {
this.markdownPreview.innerHTML = `
<div class="preview-placeholder">
<p>š Enter your Legal Markdown content and click Process to see the result</p>
</div>
`;
this.htmlPreview.innerHTML = `
<div class="preview-placeholder">
<p>š Enter your Legal Markdown content and click Process to see the result</p>
</div>
`;
}
showHelp() {
const helpContent = `š Legal Markdown Help
š Quick Start:
⢠Select an example from the dropdown to get started
⢠Or write your own content with YAML front matter
⢠Use {{variable}} syntax for template substitutions
š Document Structure:
---
title: Your Document Title
variable: value
---
# {{title}}
Your content with {{variable}} substitutions...
āØļø Keyboard Shortcuts:
⢠Ctrl+Enter: Process document
⢠Ctrl+P: Print document
⢠Ctrl+S: Download Markdown
⢠Ctrl+D: Download PDF
š§ Processing Options:
⢠Field Tracking: Highlight variable substitutions
⢠Debug Mode: Show detailed processing information
⢠Export Options: Save metadata as YAML or JSON
š” Tips:
⢠Use proper YAML indentation (spaces, not tabs)
⢠Check template brackets are balanced {{}}
⢠Use legal numbering: l., ll., lll. for sections`;
this.showMessage(helpContent, 'info');
}
handleKeyboardShortcuts(event) {
if (event.ctrlKey || event.metaKey) {
switch (event.key) {
case 'Enter':
event.preventDefault();
this.processContent();
break;
case 'p':
event.preventDefault();
this.printDocument();
break;
case 's':
event.preventDefault();
this.downloadDocument();
break;
case 'd':
event.preventDefault();
this.downloadPDF();
break;
}
}
}
showProcessing() {
this.processBtn.disabled = true;
this.processBtn.textContent = 'ā³ Processing...';
this.preview.classList.add('loading');
}
hideProcessing() {
this.processBtn.disabled = false;
this.processBtn.textContent = 'ā¶ļø Process';
this.preview.classList.remove('loading');
}
showMessage(message, type) {
const messageDiv = document.createElement('div');
messageDiv.className = `message ${type}`;
// Handle multi-line messages
if (message.includes('\n')) {
const lines = message.split('\n');
const mainMessage = lines[0];
const details = lines.slice(1).join('\n');
messageDiv.innerHTML = `
<div class="message-main">${this.escapeHtml(mainMessage)}</div>
<div class="message-details">${this.escapeHtml(details)}</div>
`;
} else {
messageDiv.textContent = message;
}
this.messageArea.innerHTML = '';
this.messageArea.appendChild(messageDiv);
// Auto-hide success messages, keep errors/warnings visible longer
const hideDelay = type === 'success' ? 3000 : 8000;
setTimeout(() => {
if (this.messageArea.contains(messageDiv)) {
messageDiv.style.opacity = '0';
setTimeout(() => {
if (this.messageArea.contains(messageDiv)) {
this.messageArea.removeChild(messageDiv);
}
}, 300);
}
}, hideDelay);
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
downloadFile(content, filename) {
const blob = new Blob([content], {
type: 'text/plain'
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
toggleCSSEditor() {
const isVisible = this.cssContent.style.display !== 'none';
if (isVisible) {
// Hide CSS editor
this.cssContent.style.display = 'none';
this.cssReset.style.display = 'none';
this.cssApply.style.display = 'none';
this.cssToggle.textContent = 'š Edit CSS';
} else {
// Show CSS editor
this.cssContent.style.display = 'block';
this.cssReset.style.display = 'inline-block';
this.cssApply.style.display = 'inline-block';
this.cssToggle.textContent = 'š½ Hide CSS';
}
}
resetCSS() {
this.cssEditor.value = '';
this.customCSS = '';
this.applyCSS();
this.showMessage('CSS reset successfully', 'success');
}
applyCSS() {
this.customCSS = this.cssEditor.value;
this.updatePreviewWithCSS();
// No mostrar mensaje en auto-apply para evitar spam
}
handleCSSFileUpload(event) {
const file = event.target.files[0];
if (!file) return;
if (!file.name.toLowerCase().endsWith('.css')) {
this.showMessage('Please select a valid CSS file (.css)', 'error');
return;
}
const reader = new FileReader();
reader.onload = (e) => {
try {
const content = e.target.result;
this.cssEditor.value = content;
this.cssExampleSelect.value = '';
this.applyCSS();
this.showMessage(`š CSS file "${file.name}" loaded successfully`, 'success');
} catch (error) {
this.showMessage('Error reading CSS file: ' + error.message, 'error');
}
};
reader.onerror = () => {
this.showMessage('Error reading CSS file. Please try again.', 'error');
};
reader.readAsText(file);
}
getCSSExamples() {
return {
'default': `/**
* Default Legal Markdown Styles
*
* Professional stylesheet for legal documents, optimized for both screen and print media.
* Features modern typography, clean design, and comprehensive print optimizations.
*/
/**
* Default Legal Markdown Styles
*
* Professional stylesheet for legal documents, optimized for both screen and print media.
* Features modern typography, clean design, and comprehensive print optimizations.
*
* Structure:
* 1. Variables and base configuration
* 2. Typography and visual hierarchy
* 3. Content elements
* 4. Tables and signatures
* 5. Print optimizations
* 6. Utilities and special classes
*/
/* Variables and base configuration
========================================================================== */
:root {
/* Colors - Professional slate and teal palette */
--color-primary: #1e293b; /* Slate 800 */
--color-accent: #0891b2; /* Cyan 600 */
--color-link: #0891b2; /* Cyan 600 */
--color-text: #334155; /* Slate 700 */
--color-error: #dc2626; /* Red 600 */
--color-bg: #ffffff; /* White */
--color-muted: #64748b; /* Slate 500 */
/* Typography */
--font-primary: -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
--font-mono: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'Courier New', monospace;
/* Assets - Default logo placeholder */
--logo-filename: https://placehold.co/120x40/2563eb/ffffff.png?text=LOGO;
/* Spacing */
--spacing-xsmall: 4px;
--spacing-small: 8px;
--spacing-medium: 16px;
--spacing-large: 32px;
--spacing-xlarge: 48px;
/* Sizes */
--font-size-base: 12pt;
--font-size-h1: 1.8em;
--font-size-h2: 1.4em;
--font-size-h3: 1.2em;
--font-size-h4: 1.1em;
/* Layout */
--max-width: 210mm; /* A4 width */
--border-radius: 6px;
--border-width: 1px;
}
/* Base document configuration
========================================================================== */
body {
font-family: var(--font-primary);
font-size: var(--font-size-base);
line-height: 1.6;
color: var(--color-text);
max-width: var(--max-width);
margin: 0 auto;
padding: 20mm 15mm;
background-color: var(--color-bg);
}
/* Typography hierarchy
========================================================================== */
/* Prevent page breaks in headings */
h1,
h2,
h3,
h4,
h5,
h6 {
page-break-after: avoid;
page-break-inside: avoid;
color: var(--color-primary);
font-weight: 600;
}
/* Main document title */
h1,
.legal-header-level-1 {
font-size: var(--font-size-h1);
line-height: 1.3;
margin: var(--spacing-medium) 0 var(--spacing-large);
text-align: center;
border-bottom: 3px solid var(--color-accent);
padding-bottom: var(--spacing-medium);
color: var(--color-primary);
}
/* Section headings */
h2,
.legal-header-level-2 {
font-size: var(--font-size-h2);
margin-top: var(--spacing-large);
margin-bottom: var(--spacing-medium);
border-bottom: 2px solid var(--color-accent);
padding-bottom: var(--spacing-small);
color: var(--color-primary);
}
/* Subsection headings */
h3,
.legal-header-level-3 {
font-size: var(--font-size-h3);
font-weight: 600;
margin-top: var(--spacing-large);
margin-bottom: var(--spacing-medium);
border-bottom: 1px solid var(--color-accent);
padding-bottom: var(--spacing-xsmall);
color: var(--color-primary);
}
/* Minor headings */
h4,
.legal-header-level-4 {
font-size: var(--font-size-h4);
font-weight: 600;
margin-top: var(--spacing-medium);
margin-bottom: var(--spacing-small);
color: var(--color-primary);
}
/* Legal headers */
h5,
h6,
.legal-header-level-5,
.legal-header-level-6 {
font-size: 1em;
font-weight: 600;
margin-top: var(--spacing-medium);
margin-bottom: var(--spacing-small);
color: var(--color-primary);
}
/* Content elements
========================================================================== */
/* Paragraphs */
p {
margin-bottom: var(--spacing-medium);
text-align: justify;
orphans: 3; /* Minimum lines at start of page */
widows: 3; /* Minimum lines at end of page */
page-break-inside: avoid;
}
/* Links */
a {
color: var(--color-link);
text-decoration: none;
transition: color 0.2s ease;
}
a:hover {
text-decoration: underline;
color: var(--color-accent);
}
/* Lists */
ul,
ol {
margin: var(--spacing-medium) 0;
padding-left: var(--spacing-large);
page-break-inside: auto;
}
li {
margin-bottom: var(--spacing-small);
page-break-inside: auto;
}
/* Code blocks */
pre {
background-color: #f8fafc;
border: var(--border-width) solid #e2e8f0;
border-radius: var(--border-radius);
padding: var(--spacing-medium);
margin: var(--spacing-medium) 0;
overflow-x: auto;
font-family: var(--font-mono);
font-size: 0.9em;
line-height: 1.4;
}
code {
background-color: #f1f5f9;
color: var(--color-primary);
padding: var(--spacing-xsmall) var(--spacing-small);
border-radius: 4px;
font-family: var(--font-mono);
font-size: 0.9em;
}
/* Horizontal separators */
hr {
border: none;
border-top: 2px solid var(--color-accent);
margin: var(--spacing-large) 0;
}
/* Empty fields and missing values */
.missing-value {
background-color: #fef2f2;
color: var(--color-error);
padding: var(--spacing-xsmall) var(--spacing-small);
border-radius: 4px;
border: var(--border-width) dashed var(--color-error);
font-style: italic;
}
/* Tables
========================================================================== */
.table-responsive {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
margin: var(--spacing-medium) 0;
}
table {
width: 100%;
border-collapse: collapse;
margin: var(--spacing-medium) 0;
table-layout: fixed;
background-color: var(--color-bg);
}
th,
td {
border: var(--border-width) solid #e2e8f0;
padding: var(--spacing-small) var(--spacing-medium);
text-align: left;
vertical-align: top;
}
th {
background-color: #f8fafc;
font-weight: 600;
color: var(--color-primary);
}
/* Signature table
========================================================================== */
.signatures {
page-break-inside: avoid;
margin-top: var(--spacing-xlarge);
border: none;
}
.signatures th {
display: none; /* Hide headers while maintaining structure */
}
.signatures th,
.signatures td {
padding: var(--spacing-medium);
text-align: left;
border: none;
border-bottom: 2px solid var(--color-primary);
}
/* Two-column layout: 1/3 for labels, 2/3 for signatures */
.signatures th:first-child,
.signatures td:first-child {
width: 33.33%;
font-weight: 600;
color: var(--color-primary);
}
.signatures th:last-child,
.signatures td:last-child {
width: 66.67%;
}
.signatures td:last-child {
height: 6em;
vertical-align: bottom;
font-weight: normal;
}
/* Table of contents
========================================================================== */
.table-of-contents {
background-color: #f8fafc;
border: var(--border-width) solid #e2e8f0;
border-radius: var(--border-radius);
padding: var(--spacing-medium);
margin: var(--spacing-large) 0;
}
.table-of-contents li {
margin-bottom: var(--spacing-small);
list-style-type: none;
}
/* Confidential section
========================================================================== */
.confidential {
width: 100%;
background-color: var(--color-accent) !important;
color: white !important;
font-weight: bold !important;
text-align: center !important;
padding: var(--spacing-medium) 0 !important;
margin: var(--spacing-large) 0 !important;
text-transform: uppercase !important;
letter-spacing: 1px !important;
display: block !important;
border-radius: var(--border-radius) !important;
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
}
/* Separator section
========================================================================== */
.separator {
width: 100%;
background-color: var(--color-primary) !important;
color: white !important;
font-weight: bold !important;
text-align: center !important;
padding: var(--spacing-small) 0 !important;
margin: var(--spacing-large) 0 var(--spacing-medium) !important;
text-transform: uppercase !important;
letter-spacing: 1px !important;
display: block !important;
border-radius: var(--border-radius) !important;
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
}
/* Algorithm blocks
========================================================================== */
.algorithm {
background-color: #f8fafc;
border: var(--border-width) solid #e2e8f0;
border-left: 4px solid var(--color-accent);
border-radius: var(--border-radius);
padding: var(--spacing-medium);
margin: var(--spacing-medium) 0;
font-family: var(--font-mono);
font-size: 0.9em;
line-height: 1.4;
}
.algorithm pre {
margin: 0;
white-space: pre-wrap;
background: none;
border: none;
padding: 0;
}
/* Utilities
========================================================================== */
.no-break {
page-break-inside: avoid !important;
}
.page-break-before {
page-break-before: always !important;
}
.text-center {
text-align: center !important;
}
.text-muted {
color: var(--color-muted) !important;
}
/* Print optimizations
========================================================================== */
@media print {
/* Page setup */
@page {
size: A4;
margin: 20mm 15mm 15mm 15mm;
}
body {
font-size: 11pt;
color: black;
max-width: 100%;
margin: 0;
padding: 0;
}
/* Links */
a {
text-decoration: none;
color: var(--color-link);
}
/* Internal links */
a[href^='#'] {
color: var(--color-link);
text-decoration: none;
}
a[data-internal-link='true'] {
color: var(--color-link) !important;
text-decoration: none !important;
}
/* Show URLs after external links */
a[href^='http']:after {
content: ' (' attr(href) ')';
font-size: 90%;
color: var(--color-muted);
}
/* Page break control */
h1, h2, h3, h4, h5, h6 {
page-break-after: avoid;
}
p, li {
page-break-inside: avoid;
}
ul, ol {
page-break-inside: auto;
}
img {
page-break-inside: avoid;
max-width: 100%;
}
/* Maintain colors in print */
.confidential,
.separator {
background-color: var(--color-accent) !important;
color: white !important;
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
}
.algorithm {
break-inside: avoid;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
/* Remove backgrounds for better printing */
pre,
code {
background-color: transparent !important;
border: 1px solid #ccc !important;
}
}
`,
'modern': `/* Modern Clean Style */
.preview-container {
font-family: 'Segoe UI', system-ui, sans-serif;
line-height: 1.6;
color: #2c3e50;
}
h1, .legal-header-level-1 {
color: #2980b9;
border-bottom: 3px solid #3498db;
padding-bottom: 10px;
font-weight: 300;
}
h2, .legal-header-level-2 {
color: #34495e;
border-left: 4px solid #3498db;
padding-left: 16px;
margin-top: 2em;
}
h3, .legal-header-level-3 {
color: #27ae60;
border-left: 3px solid #2ecc71;
padding-left: 12px;
margin-top: 1.5em;
}
h4, .legal-header-level-4 {
color: #e67e22;
border-left: 2px solid #f39c12;
padding-left: 8px;
margin-top: 1.2em;
}
p {
margin-bottom: 1.5em;
text-align: left;
}`,
'classic': `/* Classic Legal Document */
.preview-container {
font-family: 'Times New Roman', serif;
line-height: 1.8;
color: #000;
}
h1, .legal-header-level-1 {
text-align: center;
text-transform: uppercase;
letter-spacing: 2px;
border-bottom: double 3px #000;
padding-bottom: 15px;
}
h2, .legal-header-level-2 {
font-variant: small-caps;
border-bottom: 1px solid #666;
margin-top: 2em;
}
h3, .legal-header-level-3 {
font-variant: small-caps;
border-bottom: 1px dotted #999;
margin-top: 1.5em;
font-size: 1.1em;
}
h4, .legal-header-level-4 {
font-style: italic;
margin-top: 1.2em;
font-size: 1em;
}
p {
text-align: justify;
text-indent: 2em;
margin-bottom: 1em;
}`,
'minimal': `/* Minimal Style */
.preview-container {
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
line-height: 1.7;
color: #333;
max-width: none;
}
h1, .legal-header-level-1 {
font-size: 1.8em;
font-weight: 600;
color: #000;
border: none;
margin-bottom: 2em;
}
h2, .legal-header-level-2 {
font-size: 1.3em;
font-weight: 500;
color: #555;
border: none;
margin-top: 2.5em;
margin-bottom: 1em;
}
h3, .legal-header-level-3 {
font-size: 1.1em;
font-weight: 500;
color: #666;
border: none;
margin-top: 2em;
margin-bottom: 0.8em;
}
h4, .legal-header-level-4 {
font-size: 1em;
font-weight: 500;
color: #777;
border: none;
margin-top: 1.5em;
margin-bottom: 0.6em;
}
p {
margin-bottom: 1.2em;
text-align: left;
}`
};
}
loadCSSExample(exampleKey) {
if (!exampleKey) return;
const examples = this.getCSSExamples();
const example = examples[exampleKey];
if (example) {
this.cssEditor.value = example;
this.cssExampleSelect.value = exampleKey;
this.applyCSS();
this.showMessage(`šØ CSS example "${exampleKey}" loaded`, 'success');
} else {
this.showMessage('CSS example not found.', 'error');
}
}
initColumnResizing() {
const container = document.querySelector('.editor-container');
let isResizing = false;
let currentResizer = null;
let startX = 0;
let startWidths = [1, 1, 1]; // Start with equal widths
const resizers = [this.resizer1, this.resizer2];
resizers.forEach((resizer, index) => {
if (!resizer) return;
resizer.addEventListener('mousedown', (e) => {
isResizing = true;
currentResizer = resizer;
startX = e.clientX;
// Parse current fr values from the style
const currentStyle = container.style.gridTemplateColumns;
if (currentStyle && currentStyle.includes('fr')) {
const matches = currentStyle.match(/(\d*\.?\d+)fr/g);
if (matches && matches.length === 3) {
startWidths = matches.map(match => parseFloat(match.replace('fr', '')));
}
}
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
e.preventDefault();
});
});
const handleMouseMove = (e) => {
if (!isResizing || !currentResizer) return;
const deltaX = e.clientX - startX;
const containerWidth = container.offsetWidth;
// Convert pixels to fraction units (balanced sensitivity)
const deltaFraction = (deltaX / containerWidth) * 4;
let newWidths = [...startWidths];
const minWidth = 0.3; // Minimum 30% width
if (currentResizer === this.resizer1) {
// Resizing between columns 1 and 2
newWidths[0] = Math.max(minWidth, startWidths[0] + deltaFraction);
newWidths[1] = Math.max(minWidth, startWidths[1] - deltaFraction);
} else if (currentResizer === this.resizer2) {
// Resizing between columns 2 and 3
newWidths[1] = Math.max(minWidth, startWidths[1] + deltaFraction);
newWidths[2] = Math.max(minWidth, startWidths[2] - deltaFraction);
}
// Apply the new column widths
container.style.gridTemplateColumns = `${newWidths[0]}fr 4px ${newWidths[1]}fr 4px ${newWidths[2]}fr`;
};
const handleMouseUp = () => {
isResizing = false;
currentResizer = null;
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
}
initializePreviewResizer() {
if (!this.previewResizer) return;
let isResizing = false;
let startY = 0;
let startHeights = [];
this.previewResizer.addEventListener('mousedown', (e) => {
isResizing = true;
startY = e.clientY;
const markdownBlock = document.querySelector('.markdown-preview');
const htmlBlock = document.querySelector('.html-preview');
if (markdownBlock && htmlBlock) {
const markdownHeight = markdownBlock.offsetHeight;
const htmlHeight = htmlBlock.offsetHeight;
const totalHeight = markdownHeight + htmlHeight;
// Store as percentages
startHeights = [
(markdownHeight / totalHeight) * 100,
(htmlHeight / totalHeight) * 100
];
}
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
e.preventDefault();
});
const handleMouseMove = (e) => {
if (!isResizing) return;
const deltaY = e.clientY - startY;
const container = document.querySelector('.preview-split');
const containerHeight = container.offsetHeight;
// Convert pixels to percentage (balanced sensitivity)
const deltaPercent = (deltaY / containerHeight) * 100;
let newHeights = [...startHeights];
const minHeight = 20; // Minimum 20% height
// Adjust heights
newHeights[0] = Math.max(minHeight, Math.min(80, startHeights[0] + deltaPercent));
newHeights[1] = Math.max(minHeight, Math.min(80, startHeights[1] - deltaPercent));
// Apply new heights
const markdownBlock = document.querySelector('.markdown-preview');
const htmlBlock = document.querySelector('.html-preview');
if (markdownBlock && htmlBlock) {
markdownBlock.style.flex = `0 0 ${newHeights[0]}%`;
htmlBlock.style.flex = `0 0 ${newHeights[1]}%`;
}
};
const handleMouseUp = () => {
isResizing = false;
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
}
updatePreviewWithCSS() {
// Remove existing custom style tag if it exists
const existingStyle = document.getElementById('customCSS');
if (existingStyle) {
existingStyle.remove();
}
// Add new custom CSS if provided
if (this.customCSS.trim()) {
const styleTag = document.createElement('style');
styleTag.id = 'customCSS';
styleTag.textContent = `
.preview-container {
${this.customCSS}
}
`;
document.head.appendChild(styleTag);
}
}
toggleBaseStyles() {
this.baseStylesEnabled = !this.baseStylesEnabled;
this.updateBaseStyles();
// Update button appearance
this.toggleStylesBtn.style.opacity = this.baseStylesEnabled ? '1' : '0.5';
this.toggleStylesBtn.title = this.baseStylesEnabled ?
'Disable base styles' : 'Enable base styles';
this.showMessage(
this.baseStylesEnabled ? 'Base styles enabled' : 'Base styles disabled',
'info'
);
}
updateBaseStyles() {
if (this.baseStylesEnabled) {
this.preview.classList.add('styled-preview');
} else {
this.preview.classList.remove('styled-preview');
}
}
}
// Initialize app when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
new LegalMarkdownApp();
});
</script>
</body>
</html>