All files / app/shared/components/diff-view diff-view.component.ts

94.8% Statements 73/77
95.65% Branches 22/23
92.85% Functions 13/14
93.84% Lines 61/65

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 1163x 3x 3x 3x                 3x 9x 9x   9x   9x 9x   9x 9x 9x   9x 7x 7x 7x   7x 7x                     3x 3x       9x   17x 17x   7x   7x   3x             6x 18x 6x 6x 6x   6x 12x 7x 7x 7x 7x   5x 11x 5x 1x 1x 1x   4x 4x   5x       6x       6x 6x 18x 18x 35x 35x 35x 24x       6x 5x     6x 18x 1x        
import { SidebarActionComponent } from '../sidebar-action/sidebar-action.component';
import { ChangeDetectionStrategy, Component, computed, inject, input, signal } from '@angular/core';
import { LoggerService } from '@drevo-web/core';
import { DIFF_ENGINES, DiffChange, DiffEngineEntry, escapeHtml } from '@drevo-web/shared';
 
@Component({
    selector: 'app-diff-view',
    imports: [SidebarActionComponent],
    templateUrl: './diff-view.component.html',
    styleUrl: './diff-view.component.scss',
    changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DiffViewComponent {
    readonly oldText = input.required<string>();
    readonly newText = input.required<string>();
 
    private readonly logger = inject(LoggerService).withContext('DiffViewComponent');
 
    private readonly _selectedEngine = signal<DiffEngineEntry>(DIFF_ENGINES[0]);
    private readonly _collapsed = signal(true);
 
    readonly selectedEngine = this._selectedEngine.asReadonly();
    readonly engines = DIFF_ENGINES;
    readonly collapsed = this._collapsed.asReadonly();
 
    readonly diffHtml = computed(() => {
        const oldText = this.oldText();
        const newText = this.newText();
        const engine = this._selectedEngine();
 
        const changes = engine.engine.computeDiff(oldText, newText);
        return this._collapsed() ? this.renderCollapsedDiffHtml(changes) : this.renderDiffHtml(changes);
    });
 
    toggleEngine(): void {
        const currentIndex = this.engines.indexOf(this._selectedEngine());
        const nextIndex = (currentIndex + 1) % this.engines.length;
        this._selectedEngine.set(this.engines[nextIndex]);
        this.logger.info('Diff engine changed', { engineId: this._selectedEngine().id });
    }
 
    toggleCollapsed(): void {
        this._collapsed.update(v => !v);
        this.logger.info('Collapsed mode changed', { collapsed: this._collapsed() });
    }
 
    private renderDiffHtml(changes: DiffChange[]): string {
        return changes
            .map(change => {
                const escaped = escapeHtml(change.text);
                switch (change.type) {
                    case 'insert':
                        return `<span class="diff-insert">${escaped}</span>`;
                    case 'delete':
                        return `<span class="diff-delete">${escaped}</span>`;
                    default:
                        return escaped;
                }
            })
            .join('');
    }
 
    private renderCollapsedDiffHtml(changes: DiffChange[]): string {
        const lines = this.splitChangesIntoLines(changes);
        const isChanged = lines.map(line => line.some(c => c.type !== 'equal'));
        let result = '';
        let needsNewline = false;
        let i = 0;
 
        while (i < lines.length) {
            if (isChanged[i]) {
                if (needsNewline) result += '\n';
                result += this.renderDiffHtml(lines[i]);
                needsNewline = true;
                i++;
            } else {
                let j = i;
                while (j < lines.length && !isChanged[j]) j++;
                if (j - i === 1) {
                    Eif (needsNewline) result += '\n';
                    result += this.renderDiffHtml(lines[i]);
                    needsNewline = true;
                } else {
                    result += `<div class="diff-collapsed-lines">Строк без изменений: ${j - i}</div>`;
                    needsNewline = false;
                }
                i = j;
            }
        }
 
        return result;
    }
 
    private splitChangesIntoLines(changes: DiffChange[]): DiffChange[][] {
        const lines: DiffChange[][] = [[]];
        for (const change of changes) {
            const parts = change.text.split('\n');
            for (let i = 0; i < parts.length; i++) {
                if (i > 0) lines.push([]);
                const text = parts[i].replace(/\r/g, '');
                if (text) {
                    lines[lines.length - 1].push({ type: change.type, text });
                }
            }
        }
        while (lines.length > 0 && lines[lines.length - 1].length === 0) {
            lines.pop();
        }
 
        return lines.map(line => {
            if (line.some(c => c.text.trim())) return line;
            return line.map(c => ({ type: 'equal' as const, text: c.text }));
        });
    }
}