@pierre/diffs
Version:
355 lines (353 loc) • 13.8 kB
JavaScript
import { DEFAULT_RENDER_RANGE, DEFAULT_THEMES, DEFAULT_TOKENIZE_MAX_LENGTH } from "../constants.js";
import { FILE_ANNOTATION_HUNK_INDEX, FILE_ANNOTATION_LINE_INDEX, getFileAnnotations, shouldRenderFileAnnotations } from "../utils/includesFileAnnotations.js";
import { iterateOverFile } from "../utils/iterateOverFile.js";
import { createGutterGap, createGutterItem, createGutterWrapper, createHastElement } from "../utils/hast_utils.js";
import { areLanguagesAttached } from "../highlighter/languages/areLanguagesAttached.js";
import { getHighlighterIfLoaded, getSharedHighlighter } from "../highlighter/shared_highlighter.js";
import { getThemes } from "../utils/getThemes.js";
import { areThemesAttached } from "../highlighter/themes/areThemesAttached.js";
import { hasResolvedThemes } from "../highlighter/themes/hasResolvedThemes.js";
import { areFileRenderOptionsEqual } from "../utils/areFileRenderOptionsEqual.js";
import { areFilesEqual } from "../utils/areFilesEqual.js";
import { areRenderRangesEqual } from "../utils/areRenderRangesEqual.js";
import { createAnnotationElement } from "../utils/createAnnotationElement.js";
import { createContentColumn } from "../utils/createContentColumn.js";
import { createFileHeaderElement } from "../utils/createFileHeaderElement.js";
import { createPreElement } from "../utils/createPreElement.js";
import { getFiletypeFromFileName } from "../utils/getFiletypeFromFileName.js";
import { getHighlighterOptions } from "../utils/getHighlighterOptions.js";
import { getLineAnnotationName } from "../utils/getLineAnnotationName.js";
import { isFilePlainText } from "../utils/isFilePlainText.js";
import { splitFileContents } from "../utils/splitFileContents.js";
import { renderFileWithHighlighter } from "../utils/renderFileWithHighlighter.js";
import { shouldUseTokenTransformer } from "../utils/shouldUseTokenTransformer.js";
import { toHtml } from "hast-util-to-html";
//#region src/renderers/FileRenderer.ts
let instanceId = -1;
var FileRenderer = class {
__id = `file-renderer:${++instanceId}`;
highlighter;
renderCache;
computedLang = "text";
lineAnnotations = {};
lineCache;
constructor(options = { theme: DEFAULT_THEMES }, onRenderUpdate, workerManager) {
this.options = options;
this.onRenderUpdate = onRenderUpdate;
this.workerManager = workerManager;
if (workerManager?.isWorkingPool() !== true) this.highlighter = areThemesAttached(options.theme ?? DEFAULT_THEMES) ? getHighlighterIfLoaded() : void 0;
}
setOptions(options) {
this.options = options;
}
mergeOptions(options) {
this.options = {
...this.options,
...options
};
}
setLineAnnotations(lineAnnotations) {
this.lineAnnotations = {};
for (const annotation of lineAnnotations) {
const arr = this.lineAnnotations[annotation.lineNumber] ?? [];
this.lineAnnotations[annotation.lineNumber] = arr;
arr.push(annotation);
}
}
cleanUp() {
this.recycle();
this.workerManager = void 0;
this.onRenderUpdate = void 0;
}
recycle() {
this.clearRenderCache();
this.highlighter = void 0;
this.workerManager?.cleanUpTasks(this);
this.lineCache = void 0;
}
clearRenderCache() {
this.renderCache = void 0;
}
hydrate(file) {
const { options } = this.getRenderOptions(file);
const massiveFile = isFileMassive(this.getOrCreateLineCache(file).length, this.getTokenizeMaxLength());
let cache = this.workerManager?.getFileResultCache(file);
if (cache != null && !areFileRenderOptionsEqual(options, cache.options)) cache = void 0;
this.renderCache ??= {
file,
options,
highlighted: !massiveFile && !isFilePlainText(file),
result: massiveFile ? void 0 : cache?.result,
renderRange: void 0
};
if (this.workerManager?.isWorkingPool() === true) {
if (this.renderCache.result == null && !massiveFile) this.workerManager.highlightFileAST(this, file);
} else if (this.highlighter == null) {
this.computedLang = file.lang ?? getFiletypeFromFileName(file.name);
this.initializeHighlighter();
}
}
getRenderOptions(file) {
const options = (() => {
if (this.workerManager?.isWorkingPool() === true) return this.workerManager.getFileRenderOptions();
const { theme = DEFAULT_THEMES, tokenizeMaxLineLength = 1e3 } = this.options;
return {
theme,
useTokenTransformer: shouldUseTokenTransformer(this.options),
tokenizeMaxLineLength
};
})();
const { renderCache } = this;
if (renderCache?.result == null) return {
options,
forceHighlight: true
};
if (!areFilesEqual(file, renderCache.file) || !areFileRenderOptionsEqual(options, renderCache.options)) return {
options,
forceHighlight: true
};
return {
options,
forceHighlight: false
};
}
getOrCreateLineCache(file) {
if (file.cacheKey == null) {
this.lineCache = void 0;
return splitFileContents(file.contents);
}
let { lineCache } = this;
if (lineCache == null || lineCache.cacheKey !== file.cacheKey) lineCache = {
cacheKey: file.cacheKey,
lines: splitFileContents(file.contents)
};
this.lineCache = lineCache;
return lineCache.lines;
}
renderFile(file = this.renderCache?.file, renderRange = DEFAULT_RENDER_RANGE) {
if (file == null) return;
let { options, forceHighlight } = this.getRenderOptions(file);
const cache = this.getMatchingWorkerResultCache(file, options);
if (cache != null && !this.hasHighlightedRenderCache(file, options)) {
this.renderCache = {
file,
highlighted: true,
renderRange: void 0,
...cache
};
forceHighlight = false;
}
this.renderCache ??= {
file,
highlighted: false,
options,
result: void 0,
renderRange: void 0
};
const lines = this.getOrCreateLineCache(file);
const hasContent = file.contents.length > 0;
const forcePlainText = !hasContent || isFilePlainText(file) || isFileMassive(lines.length, this.getTokenizeMaxLength());
const newContent = !areFilesEqual(file, this.renderCache.file);
const newRenderRange = !areRenderRangesEqual(this.renderCache.renderRange, renderRange);
if (this.workerManager?.isWorkingPool() === true) {
if (forcePlainText || this.renderCache.result == null || !this.renderCache.highlighted && (newContent || newRenderRange)) {
this.renderCache.file = file;
this.renderCache.options = options;
this.renderCache.highlighted = false;
if (this.renderCache.result == null || newContent || newRenderRange || forceHighlight) this.renderCache.result = this.workerManager.getPlainFileAST(file, renderRange.startingLine, renderRange.totalLines, lines);
this.renderCache.renderRange = renderRange;
}
if (!forcePlainText && hasContent && (!this.renderCache.highlighted || forceHighlight)) this.workerManager.highlightFileAST(this, file);
} else {
this.computedLang = file.lang ?? getFiletypeFromFileName(file.name);
const hasThemes = this.highlighter != null && areThemesAttached(options.theme);
const hasLangs = this.highlighter != null && areLanguagesAttached(this.computedLang);
const canHighlight = !forcePlainText && hasLangs;
if (this.highlighter != null && hasThemes && (forceHighlight || forcePlainText || !this.renderCache.highlighted && canHighlight || this.renderCache.result == null)) {
const { result, options: options$1 } = this.renderFileWithHighlighter(file, this.highlighter, forcePlainText || !hasLangs);
this.renderCache = {
file,
options: options$1,
highlighted: canHighlight,
result,
renderRange: void 0
};
}
if (!hasThemes || !forcePlainText && !hasLangs) this.asyncHighlight(file).then(({ result, options: options$1 }) => {
if (this.renderCache != null) this.renderCache.highlighted = false;
this.onHighlightSuccess(file, result, options$1, !forcePlainText);
});
}
return this.renderCache.result != null ? this.processFileResult(this.renderCache.file, renderRange, this.renderCache.result) : void 0;
}
async asyncRender(file, renderRange = DEFAULT_RENDER_RANGE) {
const { result } = await this.asyncHighlight(file);
return this.processFileResult(file, renderRange, result);
}
async asyncHighlight(file) {
const forcePlainText = isFileMassive(this.getOrCreateLineCache(file).length, this.getTokenizeMaxLength());
this.computedLang = forcePlainText ? "text" : file.lang ?? getFiletypeFromFileName(file.name);
const hasThemes = this.highlighter != null && hasResolvedThemes(getThemes(this.options.theme));
const hasLangs = forcePlainText || this.highlighter != null && areLanguagesAttached(this.computedLang);
if (this.highlighter == null || !hasThemes || !hasLangs) this.highlighter = await this.initializeHighlighter();
return this.renderFileWithHighlighter(file, this.highlighter, forcePlainText);
}
renderFileWithHighlighter(file, highlighter, forcePlainText = false) {
const { options } = this.getRenderOptions(file);
return {
result: renderFileWithHighlighter(file, highlighter, options, { forcePlainText }),
options
};
}
processFileResult(file, renderRange, { code, themeStyles, baseThemeType }) {
const { disableFileHeader = false } = this.options;
const contentArray = [];
const gutter = createGutterWrapper();
const lines = this.getOrCreateLineCache(file);
let rowCount = 0;
const fileLevelAnnotations = shouldRenderFileAnnotations(renderRange) ? getFileAnnotations(this.lineAnnotations) : void 0;
if (fileLevelAnnotations != null) {
gutter.children.push(createGutterGap("context", "annotation", 1));
contentArray.push(createAnnotationElement({
type: "annotation",
hunkIndex: FILE_ANNOTATION_HUNK_INDEX,
lineIndex: FILE_ANNOTATION_LINE_INDEX,
annotations: fileLevelAnnotations.map((annotation) => getLineAnnotationName(annotation))
}));
rowCount++;
}
iterateOverFile({
lines,
startingLine: renderRange.startingLine,
totalLines: renderRange.totalLines,
callback: ({ lineIndex, lineNumber }) => {
const line = code[lineIndex];
if (line == null) {
const message = "FileRenderer.processFileResult: Line doesnt exist";
console.error(message, {
name: file.name,
lineIndex,
lineNumber,
lines
});
throw new Error(message);
}
if (line != null) {
gutter.children.push(createGutterItem("context", lineNumber, `${lineIndex}`));
contentArray.push(line);
rowCount++;
const annotations = this.lineAnnotations[lineNumber];
if (annotations != null) {
gutter.children.push(createGutterGap("context", "annotation", 1));
contentArray.push(createAnnotationElement({
type: "annotation",
hunkIndex: 0,
lineIndex: lineNumber,
annotations: annotations.map((annotation) => getLineAnnotationName(annotation))
}));
rowCount++;
}
}
}
});
gutter.properties.style = `grid-row: span ${rowCount}`;
return {
gutterAST: gutter.children ?? [],
contentAST: contentArray,
preAST: this.createPreElement(lines.length),
headerAST: !disableFileHeader ? this.renderHeader(file) : void 0,
totalLines: lines.length,
rowCount,
themeStyles,
baseThemeType,
bufferBefore: renderRange.bufferBefore,
bufferAfter: renderRange.bufferAfter,
css: ""
};
}
renderHeader(file) {
const { headerRenderMode = "default", stickyHeader = false } = this.options;
return createFileHeaderElement({
fileOrDiff: file,
mode: headerRenderMode,
stickyHeader
});
}
renderFullHTML(result) {
return toHtml(this.renderFullAST(result));
}
renderFullAST(result, children = []) {
children.push(createHastElement({
tagName: "code",
children: this.renderCodeAST(result),
properties: { "data-code": "" }
}));
return {
...result.preAST,
children
};
}
renderCodeAST(result) {
const gutter = createGutterWrapper();
gutter.children = result.gutterAST;
gutter.properties.style = `grid-row: span ${result.rowCount}`;
return [gutter, createContentColumn(result.contentAST, result.rowCount)];
}
renderPartialHTML(children, includeCodeNode = false) {
if (!includeCodeNode) return toHtml(children);
return toHtml(createHastElement({
tagName: "code",
children,
properties: { "data-code": "" }
}));
}
async initializeHighlighter() {
this.highlighter = await getSharedHighlighter(getHighlighterOptions(this.computedLang, this.options));
return this.highlighter;
}
onHighlightSuccess(file, result, options, highlighted = true) {
if (this.renderCache == null) return;
const triggerRenderUpdate = !areFilesEqual(file, this.renderCache.file) || !this.renderCache.highlighted || !areFileRenderOptionsEqual(options, this.renderCache.options);
this.renderCache = {
file,
options,
highlighted,
result,
renderRange: void 0
};
if (triggerRenderUpdate) this.onRenderUpdate?.();
}
getMatchingWorkerResultCache(file, options) {
const cache = this.workerManager?.getFileResultCache(file);
if (cache == null || !areFileRenderOptionsEqual(options, cache.options)) return;
return cache;
}
hasHighlightedRenderCache(file, options) {
const { renderCache } = this;
return renderCache?.result != null && renderCache.highlighted && areFilesEqual(file, renderCache.file) && areFileRenderOptionsEqual(options, renderCache.options);
}
onHighlightError(error) {
console.error(error);
}
getTokenizeMaxLength() {
return this.options.tokenizeMaxLength ?? DEFAULT_TOKENIZE_MAX_LENGTH;
}
createPreElement(totalLines) {
const { disableLineNumbers = false, overflow = "scroll" } = this.options;
return createPreElement({
type: "file",
diffIndicators: "none",
disableBackground: true,
disableLineNumbers,
overflow,
split: false,
totalLines
});
}
};
function isFileMassive(lineCount, tokenizeMaxLength) {
return lineCount > tokenizeMaxLength;
}
//#endregion
export { FileRenderer };
//# sourceMappingURL=FileRenderer.js.map