import { ComponentBase, ComponentState } from './componentBase'; // This import decides which languages are supported by `highlight.js`. See // their docs for details: // // https://github.com/highlightjs/highlight.js#importing-the-library import hljs from 'highlight.js/lib/common'; import { Language } from 'highlight.js'; import { LayoutContext } from '../layouting'; import { getElementHeight, getElementWidth } from '../layoutHelpers'; import { copyToClipboard, firstDefined } from '../utils'; import { applyIcon } from '../designApplication'; export type CodeBlockState = ComponentState & { _type_: 'CodeBlock-builtin'; source?: string; language?: string | null; display_controls?: boolean; }; // Converts a `
` element into a code block. This is handled externally so // it can also be used by the markdown component. export function convertDivToCodeBlock( div: HTMLDivElement, sourceCode: string, language: null | string, displayControls: boolean ) { // Spawn the necessary HTML div.classList.add('rio-code-block'); div.innerHTML = `
`; let headerElement = div.querySelector( '.rio-code-block-header' ) as HTMLDivElement; let labelElement = headerElement.querySelector( '.rio-code-block-language' ) as HTMLDivElement; let copyButton = headerElement.querySelector( '.rio-code-block-copy-button' ) as HTMLButtonElement; let codeElement = div.querySelector('code') as HTMLElement; // See if hljs recognizes the language if (language !== null && hljs.getLanguage(language) === undefined) { language = null; } // Strip any empty leading/trailing lines from the source sourceCode = sourceCode ? sourceCode.replace(/^\n+|\n+$/g, '') : ''; // Add syntax highlighting and apply the source. This will also detect the // actual language. let hljsLanguage: Language | undefined = undefined; if (language === null) { let hlResult = hljs.highlightAuto(sourceCode); codeElement.innerHTML = hlResult.value; if (hlResult.language !== undefined) { hljsLanguage = hljs.getLanguage(hlResult.language); } } else { let hlResult = hljs.highlight(sourceCode, { language: language, ignoreIllegals: true, }); codeElement.innerHTML = hlResult.value; hljsLanguage = hljs.getLanguage(language); } // Are controls enabled? if (displayControls) { // What's the language's name? let languageNiceName = hljsLanguage?.name ?? ''; labelElement.textContent = languageNiceName; // Initialize the copy button applyIcon( copyButton, 'material/content-copy', 'var(--rio-local-text-color)' ); copyButton.addEventListener('click', (event) => { const codeToCopy = (codeElement as HTMLElement).textContent ?? ''; copyToClipboard(codeToCopy); copyButton.title = 'Copied!'; applyIcon( copyButton, 'material/done', 'var(--rio-local-text-color)' ); setTimeout(() => { copyButton.title = 'Copy code'; applyIcon( copyButton, 'material/content-copy', 'var(--rio-local-text-color)' ); }, 5000); event.stopPropagation(); }); } else { headerElement.remove(); } } export class CodeBlockComponent extends ComponentBase { state: Required; // Since laying out an entire codeblock may be intensive, this component // does its best not to re-layout unless needed. This is done by setting the // height request lazily, and only if the width has changed. This value here // is the component's allocated width when the height request was last set. private heightRequestAssumesWidth: number; createElement(): HTMLElement { const element = document.createElement('div'); return element; } updateElement( deltaState: CodeBlockState, latentComponents: Set ): void { // Find the value sto use let source = firstDefined(deltaState.source, this.state.source); let language = firstDefined(deltaState.language, this.state.language); let displayControls = firstDefined( deltaState.display_controls, this.state.display_controls ); // Re-create the code block convertDivToCodeBlock( this.element as HTMLDivElement, source, language, displayControls ); // Update the width request // // For some reason the element takes up the whole parent's width // without explicitly setting its width this.element.style.width = 'min-content'; this.naturalWidth = getElementWidth(this.element); // Any previously calculated height request is no longer valid this.heightRequestAssumesWidth = -1; this.makeLayoutDirty(); } updateNaturalWidth(ctx: LayoutContext): void {} updateAllocatedWidth(ctx: LayoutContext): void {} updateNaturalHeight(ctx: LayoutContext): void { // Is the previous height request still value? if (this.heightRequestAssumesWidth === this.allocatedWidth) { return; } // No, re-layout this.element.style.height = 'min-content'; this.naturalHeight = getElementHeight(this.element); this.heightRequestAssumesWidth = this.allocatedWidth; } updateAllocatedHeight(ctx: LayoutContext): void {} }