🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
128 lines
3.2 KiB
JavaScript
128 lines
3.2 KiB
JavaScript
/**
|
|
* Vanilla JS Template Renderer
|
|
* Renders posts using HTML template literals
|
|
*/
|
|
|
|
class VanillaRenderer {
|
|
constructor() {
|
|
this.templates = new Map();
|
|
this.formatters = {
|
|
formatTime: this.formatTime.bind(this),
|
|
formatTimeAgo: this.formatTimeAgo.bind(this),
|
|
formatDateTime: this.formatDateTime.bind(this),
|
|
truncate: this.truncate.bind(this),
|
|
renderMarkdown: this.renderMarkdown.bind(this)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Load a template from HTML file
|
|
*/
|
|
async loadTemplate(templateId, templatePath) {
|
|
const response = await fetch(templatePath);
|
|
const html = await response.text();
|
|
const parser = new DOMParser();
|
|
const doc = parser.parseFromString(html, 'text/html');
|
|
const template = doc.querySelector('template');
|
|
|
|
if (template) {
|
|
this.templates.set(templateId, template.innerHTML);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Render a post using a template
|
|
*/
|
|
render(templateId, postData) {
|
|
const template = this.templates.get(templateId);
|
|
if (!template) {
|
|
throw new Error(`Template ${templateId} not loaded`);
|
|
}
|
|
|
|
// Create context with data and helper functions
|
|
const context = { ...postData, ...this.formatters };
|
|
|
|
// Use Function constructor to evaluate template literal
|
|
const rendered = new Function(...Object.keys(context), `return \`${template}\`;`)(...Object.values(context));
|
|
|
|
return rendered;
|
|
}
|
|
|
|
/**
|
|
* Render multiple posts
|
|
*/
|
|
renderBatch(templateId, posts, container) {
|
|
const fragment = document.createDocumentFragment();
|
|
|
|
posts.forEach(post => {
|
|
const html = this.render(templateId, post);
|
|
const temp = document.createElement('div');
|
|
temp.innerHTML = html;
|
|
fragment.appendChild(temp.firstElementChild);
|
|
});
|
|
|
|
container.appendChild(fragment);
|
|
}
|
|
|
|
// Helper functions available in templates
|
|
|
|
formatTime(timestamp) {
|
|
const date = new Date(timestamp * 1000);
|
|
return date.toLocaleTimeString('en-US', {
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
}
|
|
|
|
formatTimeAgo(timestamp) {
|
|
const seconds = Math.floor(Date.now() / 1000 - timestamp);
|
|
|
|
const intervals = {
|
|
year: 31536000,
|
|
month: 2592000,
|
|
week: 604800,
|
|
day: 86400,
|
|
hour: 3600,
|
|
minute: 60
|
|
};
|
|
|
|
for (const [unit, secondsInUnit] of Object.entries(intervals)) {
|
|
const interval = Math.floor(seconds / secondsInUnit);
|
|
if (interval >= 1) {
|
|
return `${interval} ${unit}${interval !== 1 ? 's' : ''} ago`;
|
|
}
|
|
}
|
|
|
|
return 'just now';
|
|
}
|
|
|
|
formatDateTime(timestamp) {
|
|
const date = new Date(timestamp * 1000);
|
|
return date.toLocaleString('en-US', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
}
|
|
|
|
truncate(text, maxLength) {
|
|
if (text.length <= maxLength) return text;
|
|
return text.substring(0, maxLength).trim() + '...';
|
|
}
|
|
|
|
renderMarkdown(text) {
|
|
// Basic markdown rendering (expand as needed)
|
|
return text
|
|
.replace(/\n\n/g, '</p><p>')
|
|
.replace(/\n/g, '<br>')
|
|
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
|
.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
|
.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2" target="_blank">$1</a>');
|
|
}
|
|
}
|
|
|
|
// Export for use
|
|
export default VanillaRenderer;
|