mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 07:31:07 -05:00
274 lines
10 KiB
JavaScript
274 lines
10 KiB
JavaScript
// Enhanced Diff Viewer Component
|
|
|
|
class DiffViewer {
|
|
constructor(options = {}) {
|
|
this.renderStrategy = options.renderStrategy || 'side-by-side';
|
|
this.syntaxHighlighters = new Map();
|
|
this.commentThreads = [];
|
|
this.container = null;
|
|
this.performance = {
|
|
startTime: null,
|
|
endTime: null
|
|
};
|
|
}
|
|
|
|
initialize(containerId) {
|
|
this.container = document.getElementById(containerId);
|
|
if (!this.container) {
|
|
throw new Error(`Container element with id "${containerId}" not found`);
|
|
}
|
|
this.setupSyntaxHighlighters();
|
|
}
|
|
|
|
setupSyntaxHighlighters() {
|
|
// Set up Prism.js or similar syntax highlighting library
|
|
this.syntaxHighlighters.set('text', this.plainTextHighlighter);
|
|
this.syntaxHighlighters.set('json', this.jsonHighlighter);
|
|
this.syntaxHighlighters.set('python', this.pythonHighlighter);
|
|
}
|
|
|
|
async render(diffData) {
|
|
this.performance.startTime = performance.now();
|
|
|
|
const { changes, metadata, navigation } = diffData;
|
|
const content = this.renderStrategy === 'side-by-side'
|
|
? this.renderSideBySide(changes)
|
|
: this.renderInline(changes);
|
|
|
|
this.container.innerHTML = `
|
|
<div class="diff-viewer ${this.renderStrategy}">
|
|
<div class="diff-header">
|
|
${this.renderMetadata(metadata)}
|
|
${this.renderControls()}
|
|
</div>
|
|
<div class="diff-content">
|
|
${content}
|
|
</div>
|
|
${this.renderNavigation(navigation)}
|
|
</div>
|
|
`;
|
|
|
|
this.attachEventListeners();
|
|
await this.highlightSyntax();
|
|
|
|
this.performance.endTime = performance.now();
|
|
this.updatePerformanceMetrics();
|
|
}
|
|
|
|
renderSideBySide(changes) {
|
|
return Object.entries(changes).map(([field, change]) => `
|
|
<div class="diff-section" data-field="${field}">
|
|
<div class="diff-field-header">
|
|
<span class="field-name">${field}</span>
|
|
<span class="syntax-type">${change.syntax_type}</span>
|
|
</div>
|
|
<div class="diff-blocks">
|
|
<div class="diff-block old" data-anchor="${change.metadata.comment_anchor_id}-old">
|
|
<div class="line-numbers">
|
|
${this.renderLineNumbers(change.metadata.line_numbers.old)}
|
|
</div>
|
|
<pre><code class="language-${change.syntax_type}">${this.escapeHtml(change.old)}</code></pre>
|
|
</div>
|
|
<div class="diff-block new" data-anchor="${change.metadata.comment_anchor_id}-new">
|
|
<div class="line-numbers">
|
|
${this.renderLineNumbers(change.metadata.line_numbers.new)}
|
|
</div>
|
|
<pre><code class="language-${change.syntax_type}">${this.escapeHtml(change.new)}</code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
renderInline(changes) {
|
|
return Object.entries(changes).map(([field, change]) => `
|
|
<div class="diff-section" data-field="${field}">
|
|
<div class="diff-field-header">
|
|
<span class="field-name">${field}</span>
|
|
<span class="syntax-type">${change.syntax_type}</span>
|
|
</div>
|
|
<div class="diff-block inline" data-anchor="${change.metadata.comment_anchor_id}">
|
|
<div class="line-numbers">
|
|
${this.renderLineNumbers(change.metadata.line_numbers.new)}
|
|
</div>
|
|
<pre><code class="language-${change.syntax_type}">
|
|
${this.renderInlineDiff(change.old, change.new)}
|
|
</code></pre>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
renderMetadata(metadata) {
|
|
return `
|
|
<div class="diff-metadata">
|
|
<span class="timestamp">${new Date(metadata.timestamp).toLocaleString()}</span>
|
|
<span class="user">${metadata.user || 'Anonymous'}</span>
|
|
<span class="change-type">${this.formatChangeType(metadata.change_type)}</span>
|
|
${metadata.reason ? `<span class="reason">${metadata.reason}</span>` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
renderControls() {
|
|
return `
|
|
<div class="diff-controls">
|
|
<button class="btn-view-mode" data-mode="side-by-side">Side by Side</button>
|
|
<button class="btn-view-mode" data-mode="inline">Inline</button>
|
|
<button class="btn-collapse-all">Collapse All</button>
|
|
<button class="btn-expand-all">Expand All</button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
renderNavigation(navigation) {
|
|
return `
|
|
<div class="diff-navigation">
|
|
${navigation.prev_id ? `<button class="btn-prev" data-id="${navigation.prev_id}">Previous</button>` : ''}
|
|
${navigation.next_id ? `<button class="btn-next" data-id="${navigation.next_id}">Next</button>` : ''}
|
|
<span class="position-indicator">Change ${navigation.current_position}</span>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
renderLineNumbers(numbers) {
|
|
return numbers.map(num => `<span class="line-number">${num}</span>`).join('');
|
|
}
|
|
|
|
renderInlineDiff(oldText, newText) {
|
|
// Simple inline diff implementation - could be enhanced with more sophisticated diff algorithm
|
|
const oldLines = oldText.split('\n');
|
|
const newLines = newText.split('\n');
|
|
const diffLines = [];
|
|
|
|
for (let i = 0; i < Math.max(oldLines.length, newLines.length); i++) {
|
|
if (oldLines[i] !== newLines[i]) {
|
|
if (oldLines[i]) {
|
|
diffLines.push(`<span class="diff-removed">${this.escapeHtml(oldLines[i])}</span>`);
|
|
}
|
|
if (newLines[i]) {
|
|
diffLines.push(`<span class="diff-added">${this.escapeHtml(newLines[i])}</span>`);
|
|
}
|
|
} else if (oldLines[i]) {
|
|
diffLines.push(this.escapeHtml(oldLines[i]));
|
|
}
|
|
}
|
|
|
|
return diffLines.join('\n');
|
|
}
|
|
|
|
attachEventListeners() {
|
|
// View mode switching
|
|
this.container.querySelectorAll('.btn-view-mode').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
this.renderStrategy = btn.dataset.mode;
|
|
this.render(this.currentDiffData);
|
|
});
|
|
});
|
|
|
|
// Collapse/Expand functionality
|
|
this.container.querySelectorAll('.diff-section').forEach(section => {
|
|
section.querySelector('.diff-field-header').addEventListener('click', () => {
|
|
section.classList.toggle('collapsed');
|
|
});
|
|
});
|
|
|
|
// Navigation
|
|
this.container.querySelectorAll('.diff-navigation button').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
this.navigateToChange(btn.dataset.id);
|
|
});
|
|
});
|
|
}
|
|
|
|
async highlightSyntax() {
|
|
const codeBlocks = this.container.querySelectorAll('code[class^="language-"]');
|
|
for (const block of codeBlocks) {
|
|
const syntax = block.className.replace('language-', '');
|
|
const highlighter = this.syntaxHighlighters.get(syntax);
|
|
if (highlighter) {
|
|
await highlighter(block);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Syntax highlighters
|
|
async plainTextHighlighter(element) {
|
|
// No highlighting needed for plain text
|
|
return element;
|
|
}
|
|
|
|
async jsonHighlighter(element) {
|
|
try {
|
|
const content = element.textContent;
|
|
const parsed = JSON.parse(content);
|
|
element.textContent = JSON.stringify(parsed, null, 2);
|
|
// Apply JSON syntax highlighting classes
|
|
element.innerHTML = element.innerHTML.replace(
|
|
/"([^"]+)":/g,
|
|
'<span class="json-key">"$1":</span>'
|
|
);
|
|
} catch (e) {
|
|
console.warn('JSON parsing failed:', e);
|
|
}
|
|
return element;
|
|
}
|
|
|
|
async pythonHighlighter(element) {
|
|
// Basic Python syntax highlighting
|
|
element.innerHTML = element.innerHTML
|
|
.replace(/(def|class|import|from|return|if|else|try|except)\b/g, '<span class="keyword">$1</span>')
|
|
.replace(/(["'])(.*?)\1/g, '<span class="string">$1$2$1</span>')
|
|
.replace(/#.*/g, '<span class="comment">$&</span>');
|
|
return element;
|
|
}
|
|
|
|
updatePerformanceMetrics() {
|
|
const renderTime = this.performance.endTime - this.performance.startTime;
|
|
if (renderTime > 200) { // Performance budget: 200ms
|
|
console.warn(`Diff render time (${renderTime}ms) exceeded performance budget`);
|
|
}
|
|
}
|
|
|
|
escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
formatChangeType(type) {
|
|
const types = {
|
|
'C': 'Changed',
|
|
'D': 'Deleted',
|
|
'A': 'Added'
|
|
};
|
|
return types[type] || type;
|
|
}
|
|
|
|
addCommentThread(anchor, thread) {
|
|
this.commentThreads.push({ anchor, thread });
|
|
this.renderCommentThreads();
|
|
}
|
|
|
|
renderCommentThreads() {
|
|
this.commentThreads.forEach(({ anchor, thread }) => {
|
|
const element = this.container.querySelector(`[data-anchor="${anchor}"]`);
|
|
if (element) {
|
|
const threadElement = document.createElement('div');
|
|
threadElement.className = 'comment-thread';
|
|
threadElement.innerHTML = thread.map(comment => `
|
|
<div class="comment">
|
|
<div class="comment-header">
|
|
<span class="comment-author">${comment.author}</span>
|
|
<span class="comment-date">${new Date(comment.date).toLocaleString()}</span>
|
|
</div>
|
|
<div class="comment-content">${comment.content}</div>
|
|
</div>
|
|
`).join('');
|
|
element.appendChild(threadElement);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
export default DiffViewer; |