simplify content renderer

This commit is contained in:
Johannes Rieken
2016-01-21 16:34:19 +01:00
parent 21b2a40f98
commit 13eaadc104
4 changed files with 84 additions and 77 deletions
+65 -62
View File
@@ -6,23 +6,14 @@
'use strict';
import DOM = require('vs/base/browser/dom');
import htmlContent = require('vs/base/common/htmlContent');
import {IHTMLContentElement} from 'vs/base/common/htmlContent';
/**
* Deal with different types of content. See @renderHtml
*/
export function renderHtml2(content:string, actionCallback?:(index:number, event:DOM.IMouseEvent)=>void):Node[];
export function renderHtml2(content:htmlContent.IHTMLContentElement, actionCallback?:(index:number, event:DOM.IMouseEvent)=>void):Node[];
export function renderHtml2(content:htmlContent.IHTMLContentElement[], actionCallback?:(index:number, event:DOM.IMouseEvent)=>void):Node[];
export function renderHtml2(content:any, actionCallback?:(index:number, event:DOM.IMouseEvent)=>void):Node[] {
if (typeof content === 'string') {
return [document.createTextNode(content)];
} else if (Array.isArray(content)) {
return (<htmlContent.IHTMLContentElement[]>content).map((piece) => renderHtml(piece, actionCallback));
} else if (content) {
return [renderHtml(<htmlContent.IHTMLContentElement>content, actionCallback)];
}
return [];
export type RenderableContent = string | IHTMLContentElement | IHTMLContentElement[];
export interface RenderOptions {
actionCallback?: (index: number, event: DOM.IMouseEvent) => void;
codeBlockRenderer?: (modeId: string, value: string) => IHTMLContentElement;
}
/**
@@ -31,39 +22,51 @@ export function renderHtml2(content:any, actionCallback?:(index:number, event:DO
* @param content a html element description
* @param actionCallback a callback function for any action links in the string. Argument is the zero-based index of the clicked action.
*/
export function renderHtml(content:htmlContent.IHTMLContentElement, actionCallback?:(index:number, event:DOM.IMouseEvent)=>void, codeBlockRenderer?:(modeId:string, value:string) => htmlContent.IHTMLContentElement):Node {
export function renderHtml(content: RenderableContent, options: RenderOptions = {}): Node {
if (typeof content === 'string') {
return _renderHtml({ text: content }, options);
} else if (Array.isArray(content)) {
return _renderHtml({ children: content }, options);
} else if (content) {
return _renderHtml(content, options);
}
}
if(content.isText) {
function _renderHtml(content: IHTMLContentElement, options: RenderOptions = {}): Node {
let {codeBlockRenderer, actionCallback} = options;
if (content.isText) {
return document.createTextNode(content.text);
}
var tagName = getSafeTagName(content.tagName) || 'div';
var element = document.createElement(tagName);
if(content.className) {
if (content.className) {
element.className = content.className;
}
if(content.text) {
if (content.text) {
element.textContent = content.text;
}
if(content.style) {
if (content.style) {
element.setAttribute('style', content.style);
}
if(content.customStyle) {
if (content.customStyle) {
Object.keys(content.customStyle).forEach((key) => {
element.style[key] = content.customStyle[key];
});
}
if (content.code && codeBlockRenderer) {
let child = codeBlockRenderer(content.code.language, content.code.value);
element.appendChild(renderHtml(child, actionCallback, codeBlockRenderer));
element.appendChild(renderHtml(child, options));
}
if(content.children) {
if (content.children) {
content.children.forEach((child) => {
element.appendChild(renderHtml(child, actionCallback, codeBlockRenderer));
element.appendChild(renderHtml(child, options));
});
}
if(content.formattedText) {
if (content.formattedText) {
renderFormattedText(element, parseFormattedText(content.formattedText), actionCallback);
}
@@ -110,29 +113,29 @@ function getSafeTagName(tagName: string): string {
}
class StringStream {
private source:string;
private index:number;
private source: string;
private index: number;
constructor(source:string) {
constructor(source: string) {
this.source = source;
this.index = 0;
}
public eos():boolean {
public eos(): boolean {
return this.index >= this.source.length;
}
public next():string {
public next(): string {
var next = this.peek();
this.advance();
return next;
}
public peek():string {
public peek(): string {
return this.source[this.index];
}
public advance():void {
public advance(): void {
this.index++;
}
}
@@ -150,24 +153,24 @@ enum FormatType {
interface IFormatParseTree {
type: FormatType;
content?:string;
index?:number;
content?: string;
index?: number;
children?: IFormatParseTree[];
}
function renderFormattedText(element:Node, treeNode:IFormatParseTree, actionCallback?:(index:number, event:DOM.IMouseEvent)=>void) {
function renderFormattedText(element: Node, treeNode: IFormatParseTree, actionCallback?: (index: number, event: DOM.IMouseEvent) => void) {
var child: Node;
if(treeNode.type === FormatType.Text) {
if (treeNode.type === FormatType.Text) {
child = document.createTextNode(treeNode.content);
}
else if(treeNode.type === FormatType.Bold) {
else if (treeNode.type === FormatType.Bold) {
child = document.createElement('b');
}
else if(treeNode.type === FormatType.Italics) {
else if (treeNode.type === FormatType.Italics) {
child = document.createElement('i');
}
else if(treeNode.type === FormatType.Action) {
else if (treeNode.type === FormatType.Action) {
var a = document.createElement('a');
a.href = '#';
DOM.addStandardDisposableListener(a, 'click', (event) => {
@@ -176,37 +179,37 @@ function renderFormattedText(element:Node, treeNode:IFormatParseTree, actionCall
child = a;
}
else if(treeNode.type === FormatType.NewLine) {
else if (treeNode.type === FormatType.NewLine) {
child = document.createElement('br');
}
else if(treeNode.type === FormatType.Root) {
else if (treeNode.type === FormatType.Root) {
child = element;
}
if(element !== child) {
if (element !== child) {
element.appendChild(child);
}
if(Array.isArray(treeNode.children)) {
if (Array.isArray(treeNode.children)) {
treeNode.children.forEach((nodeChild) => {
renderFormattedText(child, nodeChild, actionCallback);
});
}
}
function parseFormattedText(content:string):IFormatParseTree {
function parseFormattedText(content: string): IFormatParseTree {
var root:IFormatParseTree = {
var root: IFormatParseTree = {
type: FormatType.Root,
children: []
};
var actionItemIndex = 0;
var current = root;
var stack:IFormatParseTree[] = [];
var stack: IFormatParseTree[] = [];
var stream = new StringStream(content);
while(!stream.eos()) {
while (!stream.eos()) {
var next = stream.next();
var isEscapedFormatType = (next === '\\' && formatTagType(stream.peek()) !== FormatType.Invalid);
@@ -214,23 +217,23 @@ function parseFormattedText(content:string):IFormatParseTree {
next = stream.next(); // unread the backslash if it escapes a format tag type
}
if(!isEscapedFormatType && isFormatTag(next) && next === stream.peek()) {
if (!isEscapedFormatType && isFormatTag(next) && next === stream.peek()) {
stream.advance();
if(current.type === FormatType.Text) {
if (current.type === FormatType.Text) {
current = stack.pop();
}
var type = formatTagType(next);
if(current.type === type || (current.type === FormatType.Action && type === FormatType.ActionClose)) {
if (current.type === type || (current.type === FormatType.Action && type === FormatType.ActionClose)) {
current = stack.pop();
} else {
var newCurrent:IFormatParseTree = {
var newCurrent: IFormatParseTree = {
type: type,
children: []
};
if(type === FormatType.Action) {
if (type === FormatType.Action) {
newCurrent.index = actionItemIndex;
actionItemIndex++;
}
@@ -239,8 +242,8 @@ function parseFormattedText(content:string):IFormatParseTree {
stack.push(current);
current = newCurrent;
}
} else if(next === '\n') {
if(current.type === FormatType.Text) {
} else if (next === '\n') {
if (current.type === FormatType.Text) {
current = stack.pop();
}
@@ -249,8 +252,8 @@ function parseFormattedText(content:string):IFormatParseTree {
});
} else {
if(current.type !== FormatType.Text) {
var textCurrent:IFormatParseTree = {
if (current.type !== FormatType.Text) {
var textCurrent: IFormatParseTree = {
type: FormatType.Text,
content: next
};
@@ -264,23 +267,23 @@ function parseFormattedText(content:string):IFormatParseTree {
}
}
if(current.type === FormatType.Text) {
if (current.type === FormatType.Text) {
current = stack.pop();
}
if(stack.length) {
if (stack.length) {
// incorrectly formatted string literal
}
return root;
}
function isFormatTag(char:string):boolean {
function isFormatTag(char: string): boolean {
return formatTagType(char) !== FormatType.Invalid;
}
function formatTagType(char:string):FormatType {
switch(char) {
function formatTagType(char: string): FormatType {
switch (char) {
case '*':
return FormatType.Bold;
case '_':
+10 -6
View File
@@ -116,9 +116,11 @@ suite("HtmlContent", () => {
var callbackCalled = false;
var result:HTMLElement = <any> renderHtml({
formattedText: '[[action]]'
}, (index) => {
assert.strictEqual(index, 0);
callbackCalled = true;
}, {
actionCallback(index) {
assert.strictEqual(index, 0);
callbackCalled = true;
}
});
assert.strictEqual(result.innerHTML, '<a href="#">action</a>');
@@ -132,9 +134,11 @@ suite("HtmlContent", () => {
var callbackCalled = false;
var result:HTMLElement = <any> renderHtml({
formattedText: '__**[[action]]**__'
}, (index) => {
assert.strictEqual(index, 0);
callbackCalled = true;
}, {
actionCallback(index) {
assert.strictEqual(index, 0);
callbackCalled = true;
}
});
assert.strictEqual(result.innerHTML, '<i><b><a href="#">action</a></b></i>');
@@ -255,9 +255,7 @@ class MarkerNavigationWidget extends ZoneWidget.ZoneWidget {
}
this._element.text(text);
var htmlElem = this._element.getHTMLElement();
HtmlContentRenderer.renderHtml2(marker.message).forEach((node) => {
htmlElem.appendChild(node);
});
htmlElem.appendChild(HtmlContentRenderer.renderHtml(marker.message));
var mode = this.editor.getModel().getMode();
this._quickFixSection.hide();
@@ -236,13 +236,15 @@ export class ModesContentHoverWidget extends HoverWidget.ContentHoverWidget {
if(msg.htmlContent && msg.htmlContent.length > 0) {
msg.htmlContent.forEach((content) => {
container.appendChild(renderHtml(content, undefined, (modeId, value) => {
let mode: Modes.IMode;
let model = this._editor.getModel();
if (!model.isDisposed()) {
mode = model.getMode();
container.appendChild(renderHtml(content, {
codeBlockRenderer: (modeId, value) => {
let mode: Modes.IMode;
let model = this._editor.getModel();
if (!model.isDisposed()) {
mode = model.getMode();
}
return tokenizeToHtmlContent(value, model.getMode());
}
return tokenizeToHtmlContent(value, model.getMode());
}));
});
} else {