import { ComponentBase, ComponentState } from "./componentBase"; import { micromark } from "micromark"; // 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 { firstDefined, hijackLinkElement } from "../utils"; import { convertDivToCodeBlock } from "./codeBlock"; export type MarkdownState = ComponentState & { _type_: "Markdown-builtin"; text?: string; default_language?: null | string; selectable?: boolean; justify?: "left" | "right" | "center" | "justify"; overflow?: "nowrap" | "wrap" | "ellipsize"; }; // Convert a Markdown string to HTML and render it in the given div. function convertMarkdown( markdownSource: string, div: HTMLElement, defaultLanguage: null | string ) { // Drop the default language if it isn't supported or recognized if ( defaultLanguage !== null && hljs.getLanguage(defaultLanguage) === undefined ) { defaultLanguage = null; } // Convert the Markdown content to HTML div.innerHTML = micromark(markdownSource); // Post-process some of the generated HTML elements enhanceCodeBlocks(div, defaultLanguage); highlightInlineCode(div, defaultLanguage); hijackLocalLinks(div); } function enhanceCodeBlocks( div: HTMLElement, defaultLanguage: string | null ): void { const codeBlocks = div.querySelectorAll("pre"); codeBlocks.forEach((preElement) => { // Create a new div to hold the code block let codeBlockElement = document.createElement("div"); preElement.parentNode!.insertBefore(codeBlockElement, preElement); // Get the text content of the code block let sourceCode = preElement.textContent ?? ""; // Was a language specified? let codeElement = preElement.firstElementChild as HTMLElement; let specifiedLanguage: string = defaultLanguage ?? ""; for (const cls of codeElement.classList) { if (cls.startsWith("language-")) { specifiedLanguage = cls.replace("language-", ""); break; } } // Rio ships with a dedicated code block component. Delegate the work to // that. convertDivToCodeBlock( codeBlockElement, sourceCode, specifiedLanguage, true ); // Delete the original code block preElement.remove(); }); } function highlightInlineCode( div: HTMLElement, defaultLanguage: string | null ): void { // Since these are very short, guessing the language probably isn't a great // idea. Only do this if a default language was specified. // // TODO: What if most code blocks had the same language specified? Use the // same one here? if (defaultLanguage !== null) { const inlineCodeBlocks = div.querySelectorAll("code"); inlineCodeBlocks.forEach((codeElement) => { let hlResult = hljs.highlight(codeElement.textContent || "", { language: defaultLanguage!, ignoreIllegals: true, }); codeElement.innerHTML = hlResult.value; }); } } function hijackLocalLinks(div: HTMLElement): void { // Clicking a link makes the browser navigate to the URL, which is // unnecessary if the URL points to the rio app - there's no need to close // the current session and create a new one. So we'll hijack all of those // links. for (let link of div.getElementsByTagName("a")) { hijackLinkElement(link); } } export class MarkdownComponent extends ComponentBase { state: Required; createElement(): HTMLElement { const element = document.createElement("div"); element.classList.add("rio-markdown"); return element; } updateElement( deltaState: MarkdownState, latentComponents: Set ): void { super.updateElement(deltaState, latentComponents); if (deltaState.text !== undefined) { // Create a new div to hold the markdown content. This is so the // layouting code can move it around as needed. let defaultLanguage = firstDefined( deltaState.default_language, this.state.default_language ); convertMarkdown(deltaState.text, this.element, defaultLanguage); } // Handle overlong text if (deltaState.overflow !== undefined) { this.element.dataset.overflow = deltaState.overflow; } // Selectable if (deltaState.selectable !== undefined) { if (deltaState.selectable) { this.element.style.pointerEvents = "auto"; this.element.style.userSelect = "auto"; } else { this.element.style.pointerEvents = "none"; this.element.style.userSelect = "none"; } } // Text alignment if (deltaState.justify !== undefined) { this.element.style.textAlign = deltaState.justify; } } }