diff --git a/angular.json b/angular.json index 924cbd9..27949fd 100644 --- a/angular.json +++ b/angular.json @@ -103,7 +103,7 @@ "projectType": "library", "root": "projects/ngx-diff", "sourceRoot": "projects/ngx-diff/src", - "prefix": "lib", + "prefix": "ngx", "architect": { "build": { "builder": "@angular-devkit/build-angular:ng-packagr", diff --git a/projects/ngx-diff/.eslintrc.json b/projects/ngx-diff/.eslintrc.json index 6eaff7b..a443403 100644 --- a/projects/ngx-diff/.eslintrc.json +++ b/projects/ngx-diff/.eslintrc.json @@ -1,18 +1,11 @@ { "extends": "../../.eslintrc.json", - "ignorePatterns": [ - "!**/*" - ], + "ignorePatterns": ["!**/*"], "overrides": [ { - "files": [ - "*.ts" - ], + "files": ["*.ts"], "parserOptions": { - "project": [ - "projects/ngx-diff/tsconfig.lib.json", - "projects/ngx-diff/tsconfig.spec.json" - ], + "project": ["projects/ngx-diff/tsconfig.lib.json", "projects/ngx-diff/tsconfig.spec.json"], "createDefaultProgram": true }, "rules": { @@ -20,7 +13,7 @@ "error", { "type": "element", - "prefix": "lib", + "prefix": "ngx", "style": "kebab-case" } ], @@ -28,7 +21,7 @@ "error", { "type": "attribute", - "prefix": "lib", + "prefix": "ngx", "style": "camelCase" } ], @@ -38,17 +31,12 @@ "accessibility": "explicit" } ], - "arrow-parens": [ - "off", - "always" - ], + "arrow-parens": ["off", "always"], "import/order": "off" } }, { - "files": [ - "*.html" - ], + "files": ["*.html"], "rules": {} } ] diff --git a/projects/ngx-diff/src/lib/components/inline-diff/inline-diff.component.scss b/projects/ngx-diff/src/lib/components/inline-diff/inline-diff.component.scss index 3b88724..3047ff8 100644 --- a/projects/ngx-diff/src/lib/components/inline-diff/inline-diff.component.scss +++ b/projects/ngx-diff/src/lib/components/inline-diff/inline-diff.component.scss @@ -18,7 +18,7 @@ --ngx-diff-delete-color: #ffd6d6; --ngx-diff-equal-color: #ffffff; --ngx-diff-mix-color: #000; - --ngx-diff-light-mix-percentage: 2%; + --ngx-diff-light-mix-percentage: 4%; --ngx-diff-heavy-mix-percentage: 10%; --ngx-diff-inserted-background-color: var(--ngx-diff-insert-color); diff --git a/projects/ngx-diff/src/lib/components/inline-diff/inline-diff.component.ts b/projects/ngx-diff/src/lib/components/inline-diff/inline-diff.component.ts index 80f6997..6f44223 100644 --- a/projects/ngx-diff/src/lib/components/inline-diff/inline-diff.component.ts +++ b/projects/ngx-diff/src/lib/components/inline-diff/inline-diff.component.ts @@ -70,6 +70,7 @@ export class InlineDiffComponent implements OnInit, OnChanges { if (type === LineDiffType.Placeholder) { this.expandPlaceholder(index, lineDiff); + this.selectedLine = undefined; } this.selectedLineChange.emit({ @@ -108,7 +109,7 @@ export class InlineDiffComponent implements OnInit, OnChanges { type: LineDiffType.Placeholder, lineNumberInOldText: null, lineNumberInNewText: null, - line: '...', + line: `... ${remainingSkippedLines.length} hidden lines ...`, args: { skippedLines: remainingSkippedLines, lineInOldText: lineInOldText + prefix.length, @@ -265,17 +266,19 @@ export class InlineDiffComponent implements OnInit, OnChanges { // Take the first 'lineContextSize' lines from this diff to provide context for the last diff this.outputEqualDiffLines(diffLines.slice(0, this.lineContextSize), diffCalculation); + const skippedLines = diffLines.slice( + this.lineContextSize, + diffLines.length - this.lineContextSize, + ); + // Output a special line indicating that some content is equal and has been skipped diffCalculation.lines.push({ type: LineDiffType.Placeholder, lineNumberInOldText: null, lineNumberInNewText: null, - line: '...', + line: `... ${skippedLines.length} hidden lines ...`, args: { - skippedLines: diffLines.slice( - this.lineContextSize, - diffLines.length - this.lineContextSize, - ), + skippedLines, lineInOldText: diffCalculation.lineInOldText, lineInNewText: diffCalculation.lineInNewText, }, diff --git a/projects/ngx-diff/src/lib/components/side-by-side-diff/side-by-side-diff.component.html b/projects/ngx-diff/src/lib/components/side-by-side-diff/side-by-side-diff.component.html new file mode 100644 index 0000000..2d1ffa4 --- /dev/null +++ b/projects/ngx-diff/src/lib/components/side-by-side-diff/side-by-side-diff.component.html @@ -0,0 +1,76 @@ +
+ @if (title) { + {{ title }}  + } + +++ {{ diffSummary.numLinesAdded }}  + --- {{ diffSummary.numLinesRemoved }} +
+@if (isContentEqual) { +
There are no changes to display.
+} +@if (!isContentEqual) { +
+ +
+ @for (lineDiff of beforeLines; track lineDiff; let idx = $index) { +
+
{{ lineDiff.lineNumber | lineNumber }}
+
+ } +
+
+
+
+ @for (lineDiff of beforeLines; track lineDiff; let idx = $index) { +
+
{{ lineDiff.line }}
+
+ } +
+
+
+ +
+ @for (lineDiff of afterLines; track lineDiff; let idx = $index) { +
+
{{ lineDiff.lineNumber | lineNumber }}
+
+ } +
+
+
+
+ @for (lineDiff of afterLines; track lineDiff; let idx = $index) { +
+
{{ lineDiff.line }}
+
+ } +
+
+
+
+} diff --git a/projects/ngx-diff/src/lib/components/side-by-side-diff/side-by-side-diff.component.scss b/projects/ngx-diff/src/lib/components/side-by-side-diff/side-by-side-diff.component.scss new file mode 100644 index 0000000..3b01979 --- /dev/null +++ b/projects/ngx-diff/src/lib/components/side-by-side-diff/side-by-side-diff.component.scss @@ -0,0 +1,181 @@ +:host { + --ngx-diff-border-color: #888888; + --ngx-diff-font-size: 0.9rem; + --ngx-diff-font-family: Consolas, Courier, monospace; + --ngx-diff-font-color: #000000; + --ngx-diff-line-number-font-color: #484848; + --ngx-diff-line-number-hover-font-color: #ffffff; + --ngx-diff-selected-border-color: #ff8000; + + --ngx-diff-line-number-width: 2rem; + --ngx-diff-border-width: 1px; + --ngx-diff-line-left-padding: 1rem; + --ngx-diff-bottom-spacer-height: 1rem; + --ngx-diff-title-bar-padding: 0.5rem; + --ngx-diff-title-font-weight: 600; + + --ngx-diff-insert-color: #d6ffd6; + --ngx-diff-delete-color: #ffd6d6; + --ngx-diff-equal-color: #ffffff; + --ngx-diff-mix-color: #000; + --ngx-diff-light-mix-percentage: 4%; + --ngx-diff-heavy-mix-percentage: 10%; + + --ngx-diff-inserted-background-color: var(--ngx-diff-insert-color); + --ngx-diff-deleted-background-color: var(--ngx-diff-delete-color); + --ngx-diff-equal-background-color: var(--ngx-diff-equal-color); + --ngx-diff-margin-background-color: color-mix(in srgb, var(--ngx-diff-equal-color), var(--ngx-diff-mix-color) var(--ngx-diff-light-mix-percentage)); + + --ngx-diff-insert-color-darker: color-mix(in srgb, var(--ngx-diff-insert-color), var(--ngx-diff-mix-color) var(--ngx-diff-light-mix-percentage)); + --ngx-diff-insert-color-darkest: color-mix(in srgb, var(--ngx-diff-insert-color), var(--ngx-diff-mix-color) var(--ngx-diff-heavy-mix-percentage)); + + --ngx-diff-delete-color-darker: color-mix(in srgb, var(--ngx-diff-delete-color), var(--ngx-diff-mix-color) var(--ngx-diff-light-mix-percentage)); + --ngx-diff-delete-color-darkest: color-mix(in srgb, var(--ngx-diff-delete-color), var(--ngx-diff-mix-color) var(--ngx-diff-heavy-mix-percentage)); + } + + div.sbs-diff-title-bar { + background-color: var(--ngx-diff-margin-background-color); + font-family: var(--ngx-diff-font-family); + font-size: var(--ngx-diff-font-size); + font-weight: var(--ngx-diff-title-font-weight); + padding: var(--ngx-diff-title-bar-padding); + } + + div.sbs-diff-no-changes-text { + font-family: var(--ngx-diff-font-family); + font-size: var(--ngx-diff-font-size); + font-weight: var(--ngx-diff-title-font-weight); + padding: var(--ngx-diff-title-bar-padding); + background-color: var(--ngx-diff-equal-background-color); + color: var(--ngx-diff-font-color); + } + + .sbs-diff-summary-lines-added { + color: var(--ngx-diff-insert-color-darkest); + } + + .sbs-diff-summary-lines-removed { + color: var(--ngx-diff-delete-color-darkest); + } + + div.sbs-diff { + display: flex; + flex-direction: row; + border: var(--ngx-diff-border-width) solid var(--ngx-diff-border-color); + font-family: var(--ngx-diff-font-family); + + div.sbs-diff-margin:last-of-type { + border-left: var(--ngx-diff-border-width) solid var(--ngx-diff-border-color); + } + } + + div.sbs-diff-content { + position: relative; + top: 0px; + left: 0px; + flex-grow: 1; + overflow-x: auto; + overflow-y: hidden; + } + + div.sbs-diff-content-wrapper { + position: absolute; + top: 0px; + left: 0px; + display: flex; + flex-direction: column; + align-items: stretch; + width: 100%; + } + + div.sbs-diff-old { + width: var(--ngx-diff-line-number-width); + text-align: center; + font-size: var(--ngx-diff-font-size); + } + + div.sbs-diff-new { + width: var(--ngx-diff-line-number-width); + text-align: center; + border-right: var(--ngx-diff-border-width) solid var(--border-color); + font-size: var(--ngx-diff-font-size); + } + + div.sbs-diff-text { + white-space: pre; + padding-left: var(--ngx-diff-line-left-padding); + font-size: var(--ngx-diff-font-size); + color: var(--ngx-diff-font-color); + } + + .sbs-diff-equal { + background-color: var(--ngx-diff-margin-background-color); + + &.line-content { + background-color: var(--ngx-diff-equal-background-color); + } + } + + .sbs-diff-delete { + background-color: var(--ngx-diff-delete-color-darker); + + &.line-content { + background-color: var(--ngx-diff-deleted-background-color); + } + } + + .sbs-diff-insert { + background-color: var(--ngx-diff-insert-color-darker); + + &.line-content { + background-color: var(--ngx-diff-inserted-background-color); + } + } + + .sbs-diff-delete > div { + display: inline-block; + } + + .sbs-diff-insert > div { + display: inline-block; + } + + .sbs-diff-equal > div { + display: inline-block; + } + + .dmp-margin-bottom-spacer { + height: var(--ngx-diff-bottom-spacer-height); + background-color: var(--ngx-diff-margin-background-color); + border-right: var(--ngx-diff-border-width) solid var(--border-color); + + &.line-content { + background-color: var(--ngx-diff-equal-background-color); + } + } + + .line-selector { + color: var(--ngx-diff-line-number-font-color); + + .sbs-diff-before, .sbs-diff-after { + width: var(--ngx-diff-line-number-width); + text-align: center; + } + + &:hover { + cursor: pointer; + color: var(--ngx-diff-line-number-hover-font-color); + } + + &.selected { + border-top: var(--ngx-diff-border-width) solid var(--ngx-diff-selected-border-color); + border-left: var(--ngx-diff-border-width) solid var(--ngx-diff-selected-border-color); + border-bottom: var(--ngx-diff-border-width) solid var(--ngx-diff-selected-border-color); + } + } + + .line-content.selected { + border-top: var(--ngx-diff-border-width) solid var(--ngx-diff-selected-border-color); + border-right: var(--ngx-diff-border-width) solid var(--ngx-diff-selected-border-color); + border-bottom: var(--ngx-diff-border-width) solid var(--ngx-diff-selected-border-color); + } diff --git a/projects/ngx-diff/src/lib/components/side-by-side-diff/side-by-side-diff.component.spec.ts b/projects/ngx-diff/src/lib/components/side-by-side-diff/side-by-side-diff.component.spec.ts new file mode 100644 index 0000000..8978376 --- /dev/null +++ b/projects/ngx-diff/src/lib/components/side-by-side-diff/side-by-side-diff.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SideBySideDiffComponent } from './side-by-side-diff.component'; + +describe('SideBySideDiffComponent', () => { + let component: SideBySideDiffComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SideBySideDiffComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(SideBySideDiffComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/ngx-diff/src/lib/components/side-by-side-diff/side-by-side-diff.component.ts b/projects/ngx-diff/src/lib/components/side-by-side-diff/side-by-side-diff.component.ts new file mode 100644 index 0000000..aa4d507 --- /dev/null +++ b/projects/ngx-diff/src/lib/components/side-by-side-diff/side-by-side-diff.component.ts @@ -0,0 +1,427 @@ +import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core'; +import { Diff, DiffOp } from 'diff-match-patch-ts'; +import { DiffMatchPatchService } from '../../services/diff-match-patch/diff-match-patch.service'; + +import { LineNumberPipe } from '../../pipes/line-number/line-number.pipe'; +import { LineDiffType } from '../../common/line-diff-type'; +import { NgClass } from '@angular/common'; +import { LineSelectEvent } from '../../common/line-select-event'; + +interface IDiffCalculation { + beforeLineNumber: number; + afterLineNumber: number; +} + +interface ILine { + type: LineDiffType; + lineNumber: number | null; + line: string | null; + cssClass: string; + args?: { + skippedLines: string[]; + beforeLineNumber: number; + afterLineNumber: number; + }; +} + +@Component({ + selector: 'ngx-side-by-side-diff', + standalone: true, + imports: [NgClass, LineNumberPipe], + templateUrl: './side-by-side-diff.component.html', + styleUrl: './side-by-side-diff.component.scss', +}) +export class SideBySideDiffComponent implements OnInit, OnChanges { + /** + * Optional title to be displayed at the top of the diff. + */ + @Input({ required: false }) + public title?: string; + + @Input({ required: true }) + public before?: string; + + @Input({ required: true }) + public after?: string; + + /** + * The number of lines of context to provide either side of a DiffOp.Insert or DiffOp.Delete diff. + * Context is taken from a DiffOp.Equal section. + */ + @Input({ required: false }) + public lineContextSize?: number; + + @Output() + public selectedLineChange = new EventEmitter(); + + public isContentEqual = false; + public diffSummary = { + numLinesAdded: 0, + numLinesRemoved: 0, + }; + + public beforeLines: ILine[] = []; + public afterLines: ILine[] = []; + public selectedLineIndex?: number; + + public constructor(private readonly dmp: DiffMatchPatchService) {} + + public ngOnInit(): void { + this.update(); + } + + public ngOnChanges(): void { + this.update(); + } + + public selectLine(index: number): void { + this.selectedLineIndex = index; + + const selectedBeforeLine = this.beforeLines[index]; + const selectedAfterLine = this.afterLines[index]; + + const type = selectedAfterLine.type; + + const line = + (type === LineDiffType.Delete ? selectedBeforeLine.line : selectedAfterLine.line) ?? ''; + + let lineNumberInOldText: number | null = null; + let lineNumberInNewText: number | null = null; + + switch (type) { + case LineDiffType.Insert: { + lineNumberInNewText = selectedAfterLine.lineNumber; + break; + } + case LineDiffType.Delete: { + lineNumberInOldText = selectedBeforeLine.lineNumber; + break; + } + case LineDiffType.Equal: { + lineNumberInOldText = selectedBeforeLine.lineNumber; + lineNumberInNewText = selectedAfterLine.lineNumber; + break; + } + } + + if (type === LineDiffType.Placeholder) { + this.expandPlaceholder(index, selectedBeforeLine); + this.selectedLineIndex = undefined; + } + + this.selectedLineChange.emit({ + index, + type, + lineNumberInOldText, + lineNumberInNewText, + line, + }); + } + + private expandPlaceholder(index: number, placeholder: ILine): void { + const replacementLines = this.getPlaceholderReplacementLines(placeholder); + + this.beforeLines.splice(index, 1, ...replacementLines.beforeLineDiffs); + this.afterLines.splice(index, 1, ...replacementLines.afterLineDiffs); + } + + private getPlaceholderReplacementLines(placeholder: ILine): { + beforeLineDiffs: ILine[]; + afterLineDiffs: ILine[]; + } { + const { skippedLines, beforeLineNumber, afterLineNumber } = placeholder.args!; + + if (this.lineContextSize && skippedLines.length > 2 * this.lineContextSize) { + const prefix = skippedLines.slice(0, this.lineContextSize); + const remainingSkippedLines = skippedLines.slice( + this.lineContextSize, + skippedLines.length - this.lineContextSize, + ); + const suffix = skippedLines.slice( + skippedLines.length - this.lineContextSize, + skippedLines.length, + ); + + const prefixLines = this.createLineDiffs(prefix, beforeLineNumber, afterLineNumber); + + const newPlaceholder: ILine = { + type: LineDiffType.Placeholder, + lineNumber: null, + line: `... ${remainingSkippedLines.length} hidden lines ...`, + args: { + skippedLines: remainingSkippedLines, + beforeLineNumber: beforeLineNumber + prefix.length, + afterLineNumber: afterLineNumber + prefix.length, + }, + cssClass: this.getCssClass(LineDiffType.Placeholder), + }; + + const numberOfPrefixAndSkippedLines = prefix.length + remainingSkippedLines.length; + + const suffixLines = this.createLineDiffs( + suffix, + beforeLineNumber + numberOfPrefixAndSkippedLines, + afterLineNumber + numberOfPrefixAndSkippedLines, + ); + + return { + beforeLineDiffs: [ + ...prefixLines.beforeLineDiffs, + newPlaceholder, + ...suffixLines.beforeLineDiffs, + ], + afterLineDiffs: [ + ...prefixLines.afterLineDiffs, + newPlaceholder, + ...suffixLines.afterLineDiffs, + ], + }; + } + + return this.createLineDiffs(skippedLines, beforeLineNumber, afterLineNumber); + } + + private createLineDiffs( + lines: string[], + startLineInOldText: number, + startLineInNewText: number, + ): { beforeLineDiffs: ILine[]; afterLineDiffs: ILine[] } { + let beforeLineNumber = startLineInOldText; + let afterLineNumber = startLineInNewText; + + const cssClass = this.getCssClass(LineDiffType.Equal); + + const beforeLineDiffs: ILine[] = []; + const afterLineDiffs: ILine[] = []; + + for (const line of lines) { + const toInsert = { + type: LineDiffType.Equal, + line, + cssClass, + }; + + beforeLineDiffs.push({ + ...toInsert, + lineNumber: beforeLineNumber, + }); + beforeLineNumber++; + + afterLineDiffs.push({ + ...toInsert, + lineNumber: afterLineNumber, + }); + afterLineNumber++; + } + + return { beforeLineDiffs, afterLineDiffs }; + } + + private update(): void { + const beforeText = this.before ?? ''; + const afterText = this.after ?? ''; + this.calculateLineDiffs(this.dmp.computeLineDiff(beforeText, afterText)); + } + + private calculateLineDiffs(diffs: Diff[]): void { + this.beforeLines = []; + this.afterLines = []; + + const diffCalculation = { + beforeLineNumber: 1, + afterLineNumber: 1, + }; + + this.isContentEqual = diffs.length === 1 && diffs[0][0] === DiffOp.Equal; + + if (this.isContentEqual) { + this.beforeLines = []; + this.afterLines = []; + this.diffSummary = { + numLinesAdded: 0, + numLinesRemoved: 0, + }; + return; + } + + for (let i = 0; i < diffs.length; i++) { + const diff = diffs[i]; + const diffLines: string[] = diff[1].split(/\r?\n/); + + // If the original line had a \r\n at the end then remove the + // empty string after it. + if (diffLines[diffLines.length - 1].length === 0) { + diffLines.pop(); + } + + switch (diff[0]) { + case DiffOp.Equal: { + const isFirstDiff = i === 0; + const isLastDiff = i === diffs.length - 1; + this.outputEqualDiff(diffLines, diffCalculation, isFirstDiff, isLastDiff); + break; + } + case DiffOp.Delete: { + this.outputDeleteDiff(diffLines, diffCalculation); + break; + } + case DiffOp.Insert: { + this.outputInsertDiff(diffLines, diffCalculation); + break; + } + } + } + + this.diffSummary = { + numLinesAdded: this.afterLines.filter((x) => x.type === LineDiffType.Insert).length, + numLinesRemoved: this.beforeLines.filter((x) => x.type === LineDiffType.Delete).length, + }; + } + + /* If the number of diffLines is greater than lineContextSize then we may need to adjust the diff + * that is output. + * > If the first diff of a document is DiffOp.Equal then the leading lines can be dropped + * leaving the last 'lineContextSize' lines for context. + * > If the last diff of a document is DiffOp.Equal then the trailing lines can be dropped + * leaving the first 'lineContextSize' lines for context. + * > If the diff is a DiffOp.Equal occurs in the middle then the diffs either side of it must be + * DiffOp.Insert or DiffOp.Delete. If it has more than 2 * 'lineContextSize' lines of content + * then the middle lines are dropped leaving the first 'lineContextSize' and last 'lineContextSize' + * lines for context. A special line is inserted with '...' indicating that content is skipped. + * + * A document cannot consist of a single Diff with DiffOp.Equal and reach this function because + * in this case the calculateLineDiff method returns early. + */ + private outputEqualDiff( + diffLines: string[], + diffCalculation: IDiffCalculation, + isFirstDiff: boolean, + isLastDiff: boolean, + ): void { + if (this.lineContextSize && diffLines.length > this.lineContextSize) { + if (isFirstDiff) { + // Take the last 'lineContextSize' lines from the first diff + const lineIncrement = diffLines.length - this.lineContextSize; + diffCalculation.beforeLineNumber += lineIncrement; + diffCalculation.afterLineNumber += lineIncrement; + diffLines = diffLines.slice(diffLines.length - this.lineContextSize, diffLines.length); + } else if (isLastDiff) { + // Take only the first 'lineContextSize' lines from the final diff + diffLines = diffLines.slice(0, this.lineContextSize); + } else if (diffLines.length > 2 * this.lineContextSize) { + // Take the first 'lineContextSize' lines from this diff to provide context for the last diff + this.outputEqualDiffLines(diffLines.slice(0, this.lineContextSize), diffCalculation); + + const skippedLines = diffLines.slice( + this.lineContextSize, + diffLines.length - this.lineContextSize, + ); + + // Output a special line indicating that some content is equal and has been skipped + const skippedLine = { + type: LineDiffType.Placeholder, + lineNumber: null, + line: `... ${skippedLines.length} hidden lines ...`, + cssClass: this.getCssClass(LineDiffType.Placeholder), + args: { + skippedLines, + beforeLineNumber: diffCalculation.beforeLineNumber, + afterLineNumber: diffCalculation.afterLineNumber, + }, + }; + + this.beforeLines.push(skippedLine); + this.afterLines.push(skippedLine); + + const numberOfSkippedLines = diffLines.length - 2 * this.lineContextSize; + diffCalculation.beforeLineNumber += numberOfSkippedLines; + diffCalculation.afterLineNumber += numberOfSkippedLines; + + // Take the last 'lineContextSize' lines from this diff to provide context for the next diff + this.outputEqualDiffLines( + diffLines.slice(diffLines.length - this.lineContextSize), + diffCalculation, + ); + // This if branch has already output the diff lines so we return early to avoid outputting the lines + // at the end of the method. + return; + } + } + this.outputEqualDiffLines(diffLines, diffCalculation); + } + + private outputEqualDiffLines(diffLines: string[], diffCalculation: IDiffCalculation): void { + for (const line of diffLines) { + this.beforeLines.push({ + type: LineDiffType.Equal, + lineNumber: diffCalculation.beforeLineNumber, + line, + cssClass: this.getCssClass(LineDiffType.Equal), + }); + + this.afterLines.push({ + type: LineDiffType.Equal, + lineNumber: diffCalculation.afterLineNumber, + line, + cssClass: this.getCssClass(LineDiffType.Equal), + }); + + diffCalculation.beforeLineNumber++; + diffCalculation.afterLineNumber++; + } + } + + private outputDeleteDiff(diffLines: string[], diffCalculation: IDiffCalculation): void { + for (const line of diffLines) { + this.beforeLines.push({ + type: LineDiffType.Delete, + lineNumber: diffCalculation.beforeLineNumber, + line, + cssClass: this.getCssClass(LineDiffType.Delete), + }); + + this.afterLines.push({ + type: LineDiffType.Delete, + lineNumber: null, + line: null, + cssClass: this.getCssClass(LineDiffType.Delete), + }); + + diffCalculation.beforeLineNumber++; + } + } + + private outputInsertDiff(diffLines: string[], diffCalculation: IDiffCalculation): void { + for (const line of diffLines) { + this.beforeLines.push({ + type: LineDiffType.Insert, + lineNumber: null, + line: null, + cssClass: this.getCssClass(LineDiffType.Insert), + }); + + this.afterLines.push({ + type: LineDiffType.Insert, + lineNumber: diffCalculation.afterLineNumber, + line, + cssClass: this.getCssClass(LineDiffType.Insert), + }); + + diffCalculation.afterLineNumber++; + } + } + + private getCssClass(type: LineDiffType): string { + switch (type) { + case LineDiffType.Placeholder: + case LineDiffType.Equal: + return 'sbs-diff-equal'; + case LineDiffType.Insert: + return 'sbs-diff-insert'; + case LineDiffType.Delete: + return 'sbs-diff-delete'; + default: + return 'unknown'; + } + } +} diff --git a/projects/ngx-diff/src/public-api.ts b/projects/ngx-diff/src/public-api.ts index 78eca50..8237195 100644 --- a/projects/ngx-diff/src/public-api.ts +++ b/projects/ngx-diff/src/public-api.ts @@ -4,6 +4,6 @@ export * from './lib/services/diff-match-patch/diff-match-patch.service'; export * from './lib/components/inline-diff/inline-diff.component'; +export * from './lib/components/side-by-side-diff/side-by-side-diff.component'; export * from './lib/common/line-select-event'; export * from './lib/common/line-diff-type'; - diff --git a/src/app/app.component.html b/src/app/app.component.html index 01d5326..ef82f98 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -36,4 +36,13 @@ style="width: 100%" (selectedLineChange)="selectedLineChange($event)" /> +
+ diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 71f0355..61aac29 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,4 +1,4 @@ -import { InlineDiffComponent } from 'ngx-diff'; +import { InlineDiffComponent, SideBySideDiffComponent } from 'ngx-diff'; import { Component } from '@angular/core'; @@ -7,7 +7,7 @@ import { Component } from '@angular/core'; templateUrl: './app.component.html', styleUrls: ['./app.component.scss'], standalone: true, - imports: [InlineDiffComponent], + imports: [InlineDiffComponent, SideBySideDiffComponent], }) export class AppComponent { public oldText = `common text diff --git a/src/index.html b/src/index.html index 9837355..a545d72 100644 --- a/src/index.html +++ b/src/index.html @@ -1,8 +1,8 @@ - + - NgxDiff + ngx-diff