2021-09-03 22:26:31 +00:00
|
|
|
import { closeTags, nodeStream, mergeStreams, getLanguage } from './highlight.js-helpers';
|
2019-12-29 22:31:32 +00:00
|
|
|
|
|
|
|
|
import { html, Diff2HtmlConfig, defaultDiff2HtmlConfig } from '../../diff2html';
|
|
|
|
|
import { DiffFile } from '../../types';
|
2021-02-20 13:43:55 +00:00
|
|
|
import { HighlightResult, HLJSApi } from 'highlight.js';
|
2019-12-29 22:31:32 +00:00
|
|
|
|
|
|
|
|
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;
|
2021-02-20 13:43:55 +00:00
|
|
|
readonly hljs: HLJSApi | null = null;
|
2019-12-29 22:31:32 +00:00
|
|
|
|
|
|
|
|
currentSelectionColumnId = -1;
|
|
|
|
|
|
2021-02-20 13:43:55 +00:00
|
|
|
constructor(target: HTMLElement, diffInput?: string | DiffFile[], config: Diff2HtmlUIConfig = {}, hljs?: HLJSApi) {
|
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 {
|
2021-03-01 22:13:44 +00:00
|
|
|
this.targetElement.querySelectorAll<HTMLElement>('.d2h-file-collapse').forEach(fileContentToggleBtn => {
|
|
|
|
|
fileContentToggleBtn.style.display = 'flex';
|
|
|
|
|
|
2021-01-23 22:16:25 +00:00
|
|
|
const toggleFileContents: (selector: string) => void = selector => {
|
|
|
|
|
const fileContents: HTMLElement | null | undefined = fileContentToggleBtn
|
2021-01-23 15:07:14 +00:00
|
|
|
.closest('.d2h-file-wrapper')
|
2021-01-23 22:16:25 +00:00
|
|
|
?.querySelector(selector);
|
2021-01-23 15:07:14 +00:00
|
|
|
|
2021-01-23 22:32:50 +00:00
|
|
|
if (fileContents !== null && fileContents !== undefined) {
|
|
|
|
|
fileContentToggleBtn.classList.toggle('d2h-selected');
|
|
|
|
|
fileContents.classList.toggle('d2h-d-none');
|
|
|
|
|
}
|
2021-01-23 22:16:25 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const toggleHandler: (e: Event) => void = e => {
|
|
|
|
|
if (fileContentToggleBtn === e.target) return;
|
2021-01-23 15:07:14 +00:00
|
|
|
|
2021-01-23 22:16:25 +00:00
|
|
|
toggleFileContents('.d2h-file-diff');
|
|
|
|
|
toggleFileContents('.d2h-files-diff');
|
2021-01-23 15:07:14 +00:00
|
|
|
};
|
|
|
|
|
|
2021-01-23 22:16:25 +00:00
|
|
|
fileContentToggleBtn.addEventListener('click', e => toggleHandler(e));
|
2021-01-23 15:07:14 +00:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
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 => {
|
2021-02-20 13:43:55 +00:00
|
|
|
// HACK: help Typescript know that `this.hljs` is defined since we already checked it
|
|
|
|
|
if (this.hljs === null) return;
|
|
|
|
|
const language = file.getAttribute('data-lang');
|
2021-09-03 22:26:31 +00:00
|
|
|
const hljsLanguage = language ? getLanguage(language) : 'plaintext';
|
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
|
|
|
|
2021-02-20 13:43:55 +00:00
|
|
|
const result: HighlightResult = closeTags(
|
|
|
|
|
this.hljs.highlight(text, {
|
2021-09-03 22:26:31 +00:00
|
|
|
language: hljsLanguage,
|
2021-02-20 13:43:55 +00:00
|
|
|
ignoreIllegals: true,
|
|
|
|
|
}),
|
|
|
|
|
);
|
2019-12-29 22:31:32 +00:00
|
|
|
|
|
|
|
|
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-01-08 22:40:46 +00:00
|
|
|
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
|
|
|
}
|