UNPKG

@pierre/diffs

Version:

250 lines (248 loc) 9.7 kB
import { DEFAULT_THEMES, DIFFS_TAG_NAME } from "../constants.js"; import { queueRender } from "../managers/UniversalRenderingManager.js"; import { getSharedHighlighter } from "../highlighter/shared_highlighter.js"; import { getHighlighterOptions } from "../utils/getHighlighterOptions.js"; import { formatCSSVariablePrefix } from "../utils/formatCSSVariablePrefix.js"; import { getHighlighterThemeStyles } from "../utils/getHighlighterThemeStyles.js"; import { getMeasuredScrollbarGutter } from "../utils/scrollbarGutter.js"; import { wrapThemeCSS } from "../utils/cssWrappers.js"; import { getOrCreateCodeNode } from "../utils/getOrCreateCodeNode.js"; import { upsertHostThemeStyle } from "../utils/hostTheme.js"; import { setPreNodeProperties } from "../utils/setWrapperNodeProps.js"; import { CodeToTokenTransformStream } from "../shiki-stream/stream.js"; import { createSpanFromToken } from "../utils/createSpanNodeFromToken.js"; //#region src/components/FileStream.ts let instanceId = -1; var FileStream = class { __id = `file-stream:${++instanceId}`; highlighter; stream; abortController; fileContainer; pre; code; gutterElement; contentElement; themeCSSStyle; appliedThemeCSS; currentRowCount = 0; constructor(options = { theme: DEFAULT_THEMES }) { this.options = options; this.currentLineIndex = this.options.startingLineIndex ?? 1; } cleanUp() { this.abortController?.abort(); this.abortController = void 0; } setThemeType(themeType) { if ((this.options.themeType ?? "system") === themeType) return; this.options = { ...this.options, themeType }; if (typeof this.options.theme === "string" || this.fileContainer == null || this.appliedThemeCSS == null) return; this.applyThemeState(this.fileContainer, this.appliedThemeCSS.themeStyles, themeType, this.appliedThemeCSS.baseThemeType); } async initializeHighlighter() { this.highlighter = await getSharedHighlighter(getHighlighterOptions(this.options.lang, this.options)); return this.highlighter; } queuedSetupArgs; async setup(_source, _wrapper) { const isSettingUp = this.queuedSetupArgs != null; this.queuedSetupArgs = [_source, _wrapper]; if (isSettingUp) return; this.highlighter ??= await this.initializeHighlighter(); const [source, wrapper] = this.queuedSetupArgs; this.queuedSetupArgs = void 0; const stream = source; this.setupStream(stream, wrapper, this.highlighter); } setupStream(stream, wrapper, highlighter) { const { disableLineNumbers = false, overflow = "scroll", theme = DEFAULT_THEMES, themeType = "system" } = this.options; const fileContainer = this.getOrCreateFileContainer(); if (fileContainer.parentElement == null) wrapper.appendChild(fileContainer); this.pre ??= document.createElement("pre"); if (this.pre.parentElement == null) fileContainer.shadowRoot?.appendChild(this.pre); const baseThemeType = typeof theme === "string" ? highlighter.getTheme(theme).type : void 0; const themeStyles = getHighlighterThemeStyles({ theme, highlighter }); this.applyThemeState(fileContainer, themeStyles, themeType, baseThemeType); const pre = setPreNodeProperties(this.pre, { type: "file", diffIndicators: "none", disableBackground: true, disableLineNumbers, overflow, split: false, totalLines: 0 }); pre.textContent = ""; this.pre = pre; this.code = getOrCreateCodeNode({ code: this.code, pre }); this.gutterElement = void 0; this.contentElement = void 0; this.currentRowCount = 0; this.currentLineElement = void 0; this.currentLineIndex = this.options.startingLineIndex ?? 1; this.abortController?.abort(); this.abortController = new AbortController(); const { onStreamStart, onStreamClose, onStreamAbort } = this.options; this.stream?.cancel().catch(() => {}); this.stream = stream; this.stream.pipeThrough(typeof theme === "string" ? new CodeToTokenTransformStream({ ...this.options, theme, highlighter, allowRecalls: true, defaultColor: false, cssVariablePrefix: formatCSSVariablePrefix("token"), tokenizeTimeLimit: 0 }) : new CodeToTokenTransformStream({ ...this.options, themes: theme, highlighter, allowRecalls: true, defaultColor: false, cssVariablePrefix: formatCSSVariablePrefix("token"), tokenizeTimeLimit: 0 })).pipeTo(new WritableStream({ start(controller) { onStreamStart?.(controller); }, close() { onStreamClose?.(); }, abort(reason) { onStreamAbort?.(reason); }, write: this.handleWrite }), { signal: this.abortController.signal }).catch((error) => { if (error.name !== "AbortError") console.error("FileStream pipe error:", error); }); } queuedTokens = []; handleWrite = (token) => { if ("recall" in token && this.queuedTokens.length >= token.recall) this.queuedTokens.length = this.queuedTokens.length - token.recall; else this.queuedTokens.push(token); queueRender(this.render); this.options.onStreamWrite?.(token); }; currentLineIndex; currentLineElement; render = () => { this.options.onPreRender?.(this); const { gutter, content } = this.getOrCreateStreamColumns(); const gutterFragment = document.createDocumentFragment(); const contentFragment = document.createDocumentFragment(); for (const token of this.queuedTokens) if ("recall" in token) { if (this.currentLineElement == null) throw new Error("FileStream.render: no current line element, shouldnt be possible to get here"); if (token.recall > this.currentLineElement.childNodes.length) throw new Error(`FileStream.render: Token recall exceed the current line, there's probably a bug...`); for (let i = 0; i < token.recall; i++) this.currentLineElement.lastChild?.remove(); } else { const span = createSpanFromToken(token); if (this.currentLineElement == null) { const { gutterLine, contentLine } = this.createLine(); gutterFragment.appendChild(gutterLine); contentFragment.appendChild(contentLine); } this.currentLineElement?.appendChild(span); if (token.content === "\n") { this.currentLineIndex++; const { gutterLine, contentLine } = this.createLine(); gutterFragment.appendChild(gutterLine); contentFragment.appendChild(contentLine); } } if (gutterFragment.childNodes.length > 0) gutter.appendChild(gutterFragment); if (contentFragment.childNodes.length > 0) content.appendChild(contentFragment); this.queuedTokens.length = 0; this.options.onPostRender?.(this); }; getOrCreateStreamColumns() { if (this.code == null) throw new Error("FileStream: expected code element to exist"); if (this.gutterElement != null && this.contentElement != null) return { gutter: this.gutterElement, content: this.contentElement }; const gutter = document.createElement("div"); gutter.dataset.gutter = ""; const content = document.createElement("div"); content.dataset.content = ""; this.code.appendChild(gutter); this.code.appendChild(content); this.gutterElement = gutter; this.contentElement = content; return { gutter, content }; } updateRowSpan() { if (this.gutterElement != null) this.gutterElement.style.gridRow = `span ${this.currentRowCount}`; if (this.contentElement != null) this.contentElement.style.gridRow = `span ${this.currentRowCount}`; } createLine() { const lineNumber = this.currentLineIndex; const lineIndex = `${lineNumber - 1}`; const gutterLine = document.createElement("div"); gutterLine.dataset.columnNumber = `${lineNumber}`; gutterLine.dataset.lineType = "context"; gutterLine.dataset.lineIndex = lineIndex; const numberContent = document.createElement("span"); numberContent.dataset.lineNumberContent = ""; numberContent.textContent = `${lineNumber}`; gutterLine.appendChild(numberContent); const contentLine = document.createElement("div"); contentLine.dataset.line = `${lineNumber}`; contentLine.dataset.lineType = "context"; contentLine.dataset.lineIndex = lineIndex; this.currentRowCount += 1; this.updateRowSpan(); this.currentLineElement = contentLine; return { gutterLine, contentLine }; } getOrCreateFileContainer(fileContainer) { if (fileContainer != null && fileContainer === this.fileContainer || fileContainer == null && this.fileContainer != null) return this.fileContainer; if (this.fileContainer != null && fileContainer != null && fileContainer !== this.fileContainer) { this.themeCSSStyle = void 0; this.appliedThemeCSS = void 0; } this.fileContainer = fileContainer ?? document.createElement(DIFFS_TAG_NAME); return this.fileContainer; } applyThemeState(container, themeStyles, themeType, baseThemeType) { const shadowRoot = container.shadowRoot ?? container.attachShadow({ mode: "open" }); const effectiveThemeType = baseThemeType ?? themeType; const currentTheme = this.options.theme ?? DEFAULT_THEMES; const theme = typeof currentTheme === "string" ? currentTheme : { ...currentTheme }; const scrollbarGutter = getMeasuredScrollbarGutter(shadowRoot); if (this.themeCSSStyle?.parentNode === shadowRoot && this.appliedThemeCSS?.themeStyles === themeStyles && this.appliedThemeCSS.themeType === effectiveThemeType && this.appliedThemeCSS.scrollbarGutter === scrollbarGutter) { this.appliedThemeCSS.theme = theme; return; } this.themeCSSStyle = upsertHostThemeStyle({ shadowRoot, currentNode: this.themeCSSStyle, themeCSS: wrapThemeCSS(themeStyles, effectiveThemeType, scrollbarGutter) }); this.appliedThemeCSS = this.themeCSSStyle != null ? { theme, themeStyles, themeType: effectiveThemeType, baseThemeType, scrollbarGutter } : void 0; } }; //#endregion export { FileStream }; //# sourceMappingURL=FileStream.js.map