2020-05-09 11:05:18 +00:00
|
|
|
import * as HighlightJS from 'highlight.js/lib/core';
|
2020-08-15 13:40:09 +00:00
|
|
|
// import { CompiledMode, HighlightResult, AutoHighlightResult } from 'highlight.js/lib/core.js';
|
2019-12-29 22:31:32 +00:00
|
|
|
import { nodeStream, mergeStreams } from './highlight.js-helpers';
|
|
|
|
|
|
|
|
|
|
import { html, Diff2HtmlConfig, defaultDiff2HtmlConfig } from '../../diff2html';
|
|
|
|
|
import { DiffFile } from '../../types';
|
|
|
|
|
|
|
|
|
|
export interface Diff2HtmlUIConfig extends Diff2HtmlConfig {
|
|
|
|
|
synchronisedScroll?: boolean;
|
|
|
|
|
highlight?: boolean;
|
|
|
|
|
fileListToggle?: boolean;
|
|
|
|
|
fileListStartVisible?: boolean;
|
2020-02-09 16:08:58 +00:00
|
|
|
/**
|
|
|
|
|
* @deprecated since version 3.1.0
|
|
|
|
|
* Smart selection is now enabled by default with vanilla CSS
|
|
|
|
|
*/
|
2019-12-29 22:31:32 +00:00
|
|
|
smartSelection?: boolean;
|
2021-01-23 15:07:14 +00:00
|
|
|
fileContentToggle?: boolean;
|
2019-12-29 22:31:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const defaultDiff2HtmlUIConfig = {
|
|
|
|
|
...defaultDiff2HtmlConfig,
|
|
|
|
|
synchronisedScroll: true,
|
|
|
|
|
highlight: true,
|
|
|
|
|
fileListToggle: true,
|
|
|
|
|
fileListStartVisible: false,
|
2020-02-09 16:08:58 +00:00
|
|
|
/**
|
|
|
|
|
* @deprecated since version 3.1.0
|
|
|
|
|
* Smart selection is now enabled by default with vanilla CSS
|
|
|
|
|
*/
|
2019-12-29 22:31:32 +00:00
|
|
|
smartSelection: true,
|
2021-01-23 15:07:14 +00:00
|
|
|
fileContentToggle: true,
|
2019-12-29 22:31:32 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export class Diff2HtmlUI {
|
|
|
|
|
readonly config: typeof defaultDiff2HtmlUIConfig;
|
|
|
|
|
readonly diffHtml: string;
|
|
|
|
|
readonly targetElement: HTMLElement;
|
2020-01-11 05:14:41 +00:00
|
|
|
readonly hljs: typeof HighlightJS | null = null;
|
2019-12-29 22:31:32 +00:00
|
|
|
|
|
|
|
|
currentSelectionColumnId = -1;
|
|
|
|
|
|
2020-01-08 22:40:46 +00:00
|
|
|
constructor(
|
|
|
|
|
target: HTMLElement,
|
|
|
|
|
diffInput?: string | DiffFile[],
|
|
|
|
|
config: Diff2HtmlUIConfig = {},
|
2020-01-11 05:14:41 +00:00
|
|
|
hljs?: typeof HighlightJS,
|
2020-01-08 22:40:46 +00:00
|
|
|
) {
|
2019-12-29 22:31:32 +00:00
|
|
|
this.config = { ...defaultDiff2HtmlUIConfig, ...config };
|
2020-01-08 22:40:46 +00:00
|
|
|
this.diffHtml = diffInput !== undefined ? html(diffInput, this.config) : target.innerHTML;
|
2019-12-29 22:31:32 +00:00
|
|
|
this.targetElement = target;
|
|
|
|
|
if (hljs !== undefined) this.hljs = hljs;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
draw(): void {
|
|
|
|
|
this.targetElement.innerHTML = this.diffHtml;
|
|
|
|
|
if (this.config.synchronisedScroll) this.synchronisedScroll();
|
|
|
|
|
if (this.config.highlight) this.highlightCode();
|
|
|
|
|
if (this.config.fileListToggle) this.fileListToggle(this.config.fileListStartVisible);
|
2021-01-23 15:07:14 +00:00
|
|
|
if (this.config.fileContentToggle) this.fileContentToggle();
|
2019-12-29 22:31:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
synchronisedScroll(): void {
|
|
|
|
|
this.targetElement.querySelectorAll('.d2h-file-wrapper').forEach(wrapper => {
|
2020-01-25 23:49:53 +00:00
|
|
|
const [left, right] = Array<Element>().slice.call(wrapper.querySelectorAll('.d2h-file-side-diff'));
|
|
|
|
|
|
2019-12-29 22:31:32 +00:00
|
|
|
if (left === undefined || right === undefined) return;
|
2020-01-25 23:49:53 +00:00
|
|
|
|
2019-12-29 22:31:32 +00:00
|
|
|
const onScroll = (event: Event): void => {
|
|
|
|
|
if (event === null || event.target === null) return;
|
2020-01-25 23:49:53 +00:00
|
|
|
|
2019-12-29 22:31:32 +00:00
|
|
|
if (event.target === left) {
|
|
|
|
|
right.scrollTop = left.scrollTop;
|
|
|
|
|
right.scrollLeft = left.scrollLeft;
|
|
|
|
|
} else {
|
|
|
|
|
left.scrollTop = right.scrollTop;
|
|
|
|
|
left.scrollLeft = right.scrollLeft;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
left.addEventListener('scroll', onScroll);
|
|
|
|
|
right.addEventListener('scroll', onScroll);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fileListToggle(startVisible: boolean): void {
|
2020-03-10 18:16:04 +00:00
|
|
|
const showBtn: HTMLElement | null = this.targetElement.querySelector('.d2h-show');
|
2020-01-25 23:49:53 +00:00
|
|
|
const hideBtn: HTMLElement | null = this.targetElement.querySelector('.d2h-hide');
|
|
|
|
|
const fileList: HTMLElement | null = this.targetElement.querySelector('.d2h-file-list');
|
2019-12-29 22:31:32 +00:00
|
|
|
|
|
|
|
|
if (showBtn === null || hideBtn === null || fileList === null) return;
|
|
|
|
|
|
2020-01-25 23:49:53 +00:00
|
|
|
const show: () => void = () => {
|
2019-12-29 22:31:32 +00:00
|
|
|
showBtn.style.display = 'none';
|
|
|
|
|
hideBtn.style.display = 'inline';
|
|
|
|
|
fileList.style.display = 'block';
|
2020-01-25 23:49:53 +00:00
|
|
|
};
|
2019-12-29 22:31:32 +00:00
|
|
|
|
2020-01-25 23:49:53 +00:00
|
|
|
const hide: () => void = () => {
|
2019-12-29 22:31:32 +00:00
|
|
|
showBtn.style.display = 'inline';
|
|
|
|
|
hideBtn.style.display = 'none';
|
|
|
|
|
fileList.style.display = 'none';
|
2020-01-25 23:49:53 +00:00
|
|
|
};
|
2019-12-29 22:31:32 +00:00
|
|
|
|
|
|
|
|
showBtn.addEventListener('click', () => show());
|
|
|
|
|
hideBtn.addEventListener('click', () => hide());
|
|
|
|
|
|
2020-01-25 23:49:53 +00:00
|
|
|
const hashTag = this.getHashTag();
|
2019-12-29 22:31:32 +00:00
|
|
|
if (hashTag === 'files-summary-show') show();
|
|
|
|
|
else if (hashTag === 'files-summary-hide') hide();
|
|
|
|
|
else if (startVisible) show();
|
|
|
|
|
else hide();
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-23 15:07:14 +00:00
|
|
|
fileContentToggle(): void {
|
|
|
|
|
this.targetElement.querySelectorAll('.d2h-file-collapse').forEach(fileContentToggleBtn => {
|
|
|
|
|
const toggle: (e: Event) => void = e => {
|
|
|
|
|
if (fileContentToggleBtn === e.target) return;
|
|
|
|
|
|
|
|
|
|
const fileContentLineByLine: HTMLElement | null | undefined = fileContentToggleBtn
|
|
|
|
|
.closest('.d2h-file-wrapper')
|
|
|
|
|
?.querySelector('.d2h-file-diff');
|
|
|
|
|
const fileContentSideBySide: HTMLElement | null | undefined = fileContentToggleBtn
|
|
|
|
|
.closest('.d2h-file-wrapper')
|
|
|
|
|
?.querySelector('.d2h-files-diff');
|
|
|
|
|
|
|
|
|
|
if (fileContentLineByLine !== null && fileContentLineByLine !== undefined)
|
|
|
|
|
fileContentLineByLine.style.display = fileContentLineByLine.style.display === 'none' ? '' : 'none';
|
|
|
|
|
|
|
|
|
|
if (fileContentSideBySide !== null && fileContentSideBySide !== undefined)
|
|
|
|
|
fileContentSideBySide.style.display = fileContentSideBySide.style.display === 'none' ? '' : 'none';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
fileContentToggleBtn.addEventListener('click', e => toggle(e));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-29 22:31:32 +00:00
|
|
|
highlightCode(): void {
|
|
|
|
|
if (this.hljs === null) {
|
|
|
|
|
throw new Error('Missing a `highlight.js` implementation. Please provide one when instantiating Diff2HtmlUI.');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Collect all the diff files and execute the highlight on their lines
|
|
|
|
|
const files = this.targetElement.querySelectorAll('.d2h-file-wrapper');
|
|
|
|
|
files.forEach(file => {
|
2020-08-15 13:40:09 +00:00
|
|
|
let oldLinesState: CompiledMode | Language | undefined;
|
|
|
|
|
let newLinesState: CompiledMode | Language | undefined;
|
2019-12-29 22:31:32 +00:00
|
|
|
|
|
|
|
|
// Collect all the code lines and execute the highlight on them
|
|
|
|
|
const codeLines = file.querySelectorAll('.d2h-code-line-ctn');
|
|
|
|
|
codeLines.forEach(line => {
|
|
|
|
|
// HACK: help Typescript know that `this.hljs` is defined since we already checked it
|
|
|
|
|
if (this.hljs === null) return;
|
|
|
|
|
|
|
|
|
|
const text = line.textContent;
|
2020-01-25 23:49:53 +00:00
|
|
|
const lineParent = line.parentNode;
|
2019-12-29 22:31:32 +00:00
|
|
|
|
2020-01-25 23:49:53 +00:00
|
|
|
if (text === null || lineParent === null || !this.isElement(lineParent)) return;
|
2019-12-29 22:31:32 +00:00
|
|
|
|
2020-01-25 23:49:53 +00:00
|
|
|
const lineState = lineParent.classList.contains('d2h-del') ? oldLinesState : newLinesState;
|
2019-12-29 22:31:32 +00:00
|
|
|
|
|
|
|
|
const language = file.getAttribute('data-lang');
|
2020-08-15 13:40:09 +00:00
|
|
|
const result: HighlightResult =
|
2019-12-29 22:31:32 +00:00
|
|
|
language && this.hljs.getLanguage(language)
|
|
|
|
|
? this.hljs.highlight(language, text, true, lineState)
|
|
|
|
|
: this.hljs.highlightAuto(text);
|
|
|
|
|
|
2020-08-15 13:40:09 +00:00
|
|
|
if (this.instanceOfHighlightResult(result)) {
|
2020-01-25 23:49:53 +00:00
|
|
|
if (lineParent.classList.contains('d2h-del')) {
|
2019-12-29 22:31:32 +00:00
|
|
|
oldLinesState = result.top;
|
2020-01-25 23:49:53 +00:00
|
|
|
} else if (lineParent.classList.contains('d2h-ins')) {
|
2019-12-29 22:31:32 +00:00
|
|
|
newLinesState = result.top;
|
|
|
|
|
} else {
|
|
|
|
|
oldLinesState = result.top;
|
|
|
|
|
newLinesState = result.top;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const originalStream = nodeStream(line);
|
|
|
|
|
if (originalStream.length) {
|
|
|
|
|
const resultNode = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
|
|
|
|
|
resultNode.innerHTML = result.value;
|
|
|
|
|
result.value = mergeStreams(originalStream, nodeStream(resultNode), text);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
line.classList.add('hljs');
|
2020-08-15 13:40:09 +00:00
|
|
|
if (result.language) {
|
|
|
|
|
line.classList.add(result.language);
|
|
|
|
|
}
|
2019-12-29 22:31:32 +00:00
|
|
|
line.innerHTML = result.value;
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-09 16:08:58 +00:00
|
|
|
/**
|
|
|
|
|
* @deprecated since version 3.1.0
|
|
|
|
|
*/
|
2020-01-08 22:40:46 +00:00
|
|
|
smartSelection(): void {
|
2020-02-09 16:08:58 +00:00
|
|
|
console.warn('Smart selection is now enabled by default with CSS. No need to call this method anymore.');
|
2019-12-29 22:31:32 +00:00
|
|
|
}
|
|
|
|
|
|
2020-08-15 13:40:09 +00:00
|
|
|
private instanceOfHighlightResult(object: HighlightResult | AutoHighlightResult): object is HighlightResult {
|
2020-01-08 22:40:46 +00:00
|
|
|
return 'top' in object;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private getHashTag(): string | null {
|
|
|
|
|
const docUrl = document.URL;
|
|
|
|
|
const hashTagIndex = docUrl.indexOf('#');
|
|
|
|
|
|
|
|
|
|
let hashTag = null;
|
|
|
|
|
if (hashTagIndex !== -1) {
|
|
|
|
|
hashTag = docUrl.substr(hashTagIndex + 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return hashTag;
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-25 23:49:53 +00:00
|
|
|
private isElement(arg?: unknown): arg is Element {
|
|
|
|
|
return arg !== null && (arg as Element)?.classList !== undefined;
|
|
|
|
|
}
|
2019-12-29 22:31:32 +00:00
|
|
|
}
|