mirror of
https://github.com/rio-labs/rio.git
synced 2025-12-31 18:29:44 -06:00
203 lines
6.2 KiB
TypeScript
203 lines
6.2 KiB
TypeScript
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;
|
|
};
|
|
|
|
/// Contains additional aliases for languages that are not recognized by
|
|
/// highlight.js
|
|
const languageAliases: { [key: string]: string } = {
|
|
none: 'plaintext',
|
|
null: 'plaintext',
|
|
plain: 'plaintext',
|
|
};
|
|
|
|
// Converts a `<div>` 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 = `
|
|
<div class="rio-code-block-header">
|
|
<div class="rio-code-block-language"></div>
|
|
<button class="rio-code-block-copy-button">Copy code</button>
|
|
</div>
|
|
<pre></pre>
|
|
`;
|
|
|
|
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 preElement = div.querySelector('pre') as HTMLPreElement;
|
|
|
|
// Support additional language aliases
|
|
if (language !== null && languageAliases[language] !== undefined) {
|
|
language = languageAliases[language];
|
|
}
|
|
|
|
// 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);
|
|
preElement.innerHTML = hlResult.value;
|
|
|
|
if (hlResult.language !== undefined) {
|
|
hljsLanguage = hljs.getLanguage(hlResult.language);
|
|
}
|
|
} else {
|
|
let hlResult = hljs.highlight(sourceCode, {
|
|
language: language,
|
|
ignoreIllegals: true,
|
|
});
|
|
preElement.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 = (preElement as HTMLPreElement).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<CodeBlockState>;
|
|
|
|
// 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<ComponentBase>
|
|
): 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 {}
|
|
}
|