Path: blob/main/src/vs/base/browser/formattedTextRenderer.ts
3292 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as DOM from './dom.js';6import { IKeyboardEvent } from './keyboardEvent.js';7import { IMouseEvent } from './mouseEvent.js';8import { DisposableStore } from '../common/lifecycle.js';910export interface IContentActionHandler {11readonly callback: (content: string, event: IMouseEvent | IKeyboardEvent) => void;12readonly disposables: DisposableStore;13}1415export interface FormattedTextRenderOptions {16readonly actionHandler?: IContentActionHandler;17readonly renderCodeSegments?: boolean;18}1920export function renderText(text: string, _options?: FormattedTextRenderOptions, target?: HTMLElement): HTMLElement {21const element = target ?? document.createElement('div');22element.textContent = text;23return element;24}2526export function renderFormattedText(formattedText: string, options?: FormattedTextRenderOptions, target?: HTMLElement): HTMLElement {27const element = target ?? document.createElement('div');28element.textContent = '';29_renderFormattedText(element, parseFormattedText(formattedText, !!options?.renderCodeSegments), options?.actionHandler, options?.renderCodeSegments);30return element;31}3233class StringStream {34private source: string;35private index: number;3637constructor(source: string) {38this.source = source;39this.index = 0;40}4142public eos(): boolean {43return this.index >= this.source.length;44}4546public next(): string {47const next = this.peek();48this.advance();49return next;50}5152public peek(): string {53return this.source[this.index];54}5556public advance(): void {57this.index++;58}59}6061const enum FormatType {62Invalid,63Root,64Text,65Bold,66Italics,67Action,68ActionClose,69Code,70NewLine71}7273interface IFormatParseTree {74type: FormatType;75content?: string;76index?: number;77children?: IFormatParseTree[];78}7980function _renderFormattedText(element: Node, treeNode: IFormatParseTree, actionHandler?: IContentActionHandler, renderCodeSegments?: boolean) {81let child: Node | undefined;8283if (treeNode.type === FormatType.Text) {84child = document.createTextNode(treeNode.content || '');85} else if (treeNode.type === FormatType.Bold) {86child = document.createElement('b');87} else if (treeNode.type === FormatType.Italics) {88child = document.createElement('i');89} else if (treeNode.type === FormatType.Code && renderCodeSegments) {90child = document.createElement('code');91} else if (treeNode.type === FormatType.Action && actionHandler) {92const a = document.createElement('a');93actionHandler.disposables.add(DOM.addStandardDisposableListener(a, 'click', (event) => {94actionHandler.callback(String(treeNode.index), event);95}));9697child = a;98} else if (treeNode.type === FormatType.NewLine) {99child = document.createElement('br');100} else if (treeNode.type === FormatType.Root) {101child = element;102}103104if (child && element !== child) {105element.appendChild(child);106}107108if (child && Array.isArray(treeNode.children)) {109treeNode.children.forEach((nodeChild) => {110_renderFormattedText(child, nodeChild, actionHandler, renderCodeSegments);111});112}113}114115function parseFormattedText(content: string, parseCodeSegments: boolean): IFormatParseTree {116117const root: IFormatParseTree = {118type: FormatType.Root,119children: []120};121122let actionViewItemIndex = 0;123let current = root;124const stack: IFormatParseTree[] = [];125const stream = new StringStream(content);126127while (!stream.eos()) {128let next = stream.next();129130const isEscapedFormatType = (next === '\\' && formatTagType(stream.peek(), parseCodeSegments) !== FormatType.Invalid);131if (isEscapedFormatType) {132next = stream.next(); // unread the backslash if it escapes a format tag type133}134135if (!isEscapedFormatType && isFormatTag(next, parseCodeSegments) && next === stream.peek()) {136stream.advance();137138if (current.type === FormatType.Text) {139current = stack.pop()!;140}141142const type = formatTagType(next, parseCodeSegments);143if (current.type === type || (current.type === FormatType.Action && type === FormatType.ActionClose)) {144current = stack.pop()!;145} else {146const newCurrent: IFormatParseTree = {147type: type,148children: []149};150151if (type === FormatType.Action) {152newCurrent.index = actionViewItemIndex;153actionViewItemIndex++;154}155156current.children!.push(newCurrent);157stack.push(current);158current = newCurrent;159}160} else if (next === '\n') {161if (current.type === FormatType.Text) {162current = stack.pop()!;163}164165current.children!.push({166type: FormatType.NewLine167});168169} else {170if (current.type !== FormatType.Text) {171const textCurrent: IFormatParseTree = {172type: FormatType.Text,173content: next174};175current.children!.push(textCurrent);176stack.push(current);177current = textCurrent;178179} else {180current.content += next;181}182}183}184185if (current.type === FormatType.Text) {186current = stack.pop()!;187}188189if (stack.length) {190// incorrectly formatted string literal191}192193return root;194}195196function isFormatTag(char: string, supportCodeSegments: boolean): boolean {197return formatTagType(char, supportCodeSegments) !== FormatType.Invalid;198}199200function formatTagType(char: string, supportCodeSegments: boolean): FormatType {201switch (char) {202case '*':203return FormatType.Bold;204case '_':205return FormatType.Italics;206case '[':207return FormatType.Action;208case ']':209return FormatType.ActionClose;210case '`':211return supportCodeSegments ? FormatType.Code : FormatType.Invalid;212default:213return FormatType.Invalid;214}215}216217218