Skip to content

Commit

Permalink
Render markdown for preferences
Browse files Browse the repository at this point in the history
  • Loading branch information
msujew committed Nov 29, 2021
1 parent 7889beb commit 1bdafc4
Show file tree
Hide file tree
Showing 18 changed files with 286 additions and 35 deletions.
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"keytar": "7.2.0",
"lodash.debounce": "^4.0.8",
"lodash.throttle": "^4.1.1",
"markdown-it": "^8.4.0",
"nsfw": "^2.1.2",
"p-debounce": "^2.1.0",
"perfect-scrollbar": "^1.3.0",
Expand Down
5 changes: 2 additions & 3 deletions packages/core/src/browser/core-preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,7 @@ export const corePreferenceSchema: PreferenceSchema = {
'keyCode',
],
default: 'code',
description: nls.localizeByDefault(
'Controls the dispatching logic for key presses to use either `code` (recommended) or `keyCode`.')
markdownDescription: nls.localizeByDefault('Controls the dispatching logic for key presses to use either `code` (recommended) or `keyCode`.')
},
'window.menuBarVisibility': {
type: 'string',
Expand Down Expand Up @@ -89,7 +88,7 @@ export const corePreferenceSchema: PreferenceSchema = {
'workbench.editor.highlightModifiedTabs': {
'type': 'boolean',
// eslint-disable-next-line max-len
'description': nls.localizeByDefault('Controls whether a top border is drawn on modified (dirty) editor tabs or not. This value is ignored when `#workbench.editor.showTabs#` is disabled.'),
'markdownDescription': nls.localize('theia/core/highlightModifiedTabs', 'Controls whether a top border is drawn on modified (dirty) editor tabs or not.'),
'default': false
},
'workbench.editor.closeOnFileDelete': {
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/browser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,4 @@ export * from './core-preferences';
export * from './view-container';
export * from './breadcrumbs';
export * from './tooltip-service';
export * from './markdown-renderer';
78 changes: 78 additions & 0 deletions packages/core/src/browser/markdown-renderer.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/********************************************************************************
* Copyright (C) 2021 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { enableJSDOM } from '../browser/test/jsdom';
let disableJSDOM = enableJSDOM();

import { expect } from 'chai';
import * as markdownit from 'markdown-it';
import { MarkdownRenderer } from './markdown-renderer';

disableJSDOM();

describe('MarkdownRenderer', () => {

before(() => disableJSDOM = enableJSDOM());
after(() => disableJSDOM());

it('Should render markdown', () => {
const markdownRenderer = new MarkdownRenderer();
const result = markdownRenderer.renderInline('[title](link)').innerHTML;
expect(result).to.be.equal('<a href="link">title</a>');
});

it('Should accept and use custom engine', () => {
const engine = markdownit();
const originalTextRenderer = engine.renderer.rules.text!;
engine.renderer.rules.text = (tokens, idx, options, env, self) => `[${originalTextRenderer(tokens, idx, options, env, self)}]`;
const markdownRenderer = new MarkdownRenderer(engine);
const result = markdownRenderer.renderInline('text').innerHTML;
expect(result).to.be.equal('[text]');
});

it('Should modify rendered markdown in place', () => {
const markdownRenderer = new MarkdownRenderer().modify('a', a => {
a.href = 'something-else';
});
const result = markdownRenderer.renderInline('[title](link)').innerHTML;
expect(result).to.be.equal('<a href="something-else">title</a>');
});

it('Should modify descendants of children', () => {
const markdownRenderer = new MarkdownRenderer().modify('em', em => {
const strong = document.createElement('strong');
// eslint-disable-next-line no-unsanitized/property
strong.innerHTML = em.innerHTML;
return strong;
});
const result = markdownRenderer.render('**bold *bold and italic***').innerHTML;
expect(result).to.be.equal('<p><strong>bold <strong>bold and italic</strong></strong></p>\n');
});

it('Should modify descendants of children after previous modification', () => {
const markdownRenderer = new MarkdownRenderer().modify('em', em => {
const strong = document.createElement('strong');
// eslint-disable-next-line no-unsanitized/property
strong.innerHTML = em.innerHTML;
return strong;
}).modify('strong', strong => { // Will pick up both the original and modified strong element
const textNode = strong.childNodes.item(0);
textNode.textContent = `changed_${textNode.textContent}`;
});
const result = markdownRenderer.render('**bold *bold and italic***').innerHTML;
expect(result).to.be.equal('<p><strong>changed_bold <strong>changed_bold and italic</strong></strong></p>\n');
});
});
76 changes: 76 additions & 0 deletions packages/core/src/browser/markdown-renderer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/********************************************************************************
* Copyright (C) 2021 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import * as DOMPurify from 'dompurify';
import * as markdownit from 'markdown-it';

export class MarkdownRenderer {

protected engine: markdownit;
protected callbacks = new Map<string, ((element: Element) => Element | void)[]>();

constructor(engine?: markdownit) {
this.engine = engine ?? markdownit();
}

/**
* Adds a modification callback that is applied to every element with the specified tag after rendering to HTML.
*
* @param tag The tag that this modification applies to.
* @param callback The modification to apply on every selected rendered element. Can either modify the element in place or return a new element.
*/
modify<K extends keyof HTMLElementTagNameMap>(tag: K, callback: (element: HTMLElementTagNameMap[K]) => Element | void): MarkdownRenderer {
if (this.callbacks.has(tag)) {
this.callbacks.get(tag)!.push(callback);
} else {
this.callbacks.set(tag, [callback]);
}
return this;
}

render(markdown: string): HTMLElement {
return this.renderInternal(this.engine.render(markdown));
}

renderInline(markdown: string): HTMLElement {
return this.renderInternal(this.engine.renderInline(markdown));
}

protected renderInternal(renderedHtml: string): HTMLElement {
const div = this.sanitizeHtml(renderedHtml);
for (const [tag, calls] of this.callbacks) {
for (const callback of calls) {
const elements = Array.from(div.getElementsByTagName(tag));
for (const element of elements) {
const result = callback(element);
if (result) {
const parent = element.parentElement;
if (parent) {
parent.replaceChild(result, element);
}
}
}
}
}
return div;
}

protected sanitizeHtml(html: string): HTMLElement {
const div = document.createElement('div');
div.innerHTML = DOMPurify.sanitize(html);
return div;
}
}
12 changes: 6 additions & 6 deletions packages/editor/src/browser/editor-preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ const codeEditorPreferenceProperties = {
},
'editor.semanticHighlighting.enabled': {
'enum': [true, false, 'configuredByTheme'],
'enumDescriptions': [
'markdownEnumDescriptions': [
nls.localizeByDefault('Semantic highlighting enabled for all color themes.'),
nls.localizeByDefault('Semantic highlighting disabled for all color themes.'),
nls.localizeByDefault('Semantic highlighting is configured by the current color theme\'s `semanticHighlighting` setting.')
Expand Down Expand Up @@ -327,7 +327,7 @@ const codeEditorPreferenceProperties = {
'default': 0,
'minimum': 0,
'maximum': 100,
'description': nls.localizeByDefault('Controls the font size in pixels for CodeLens. When set to `0`, the 90% of `#editor.fontSize#` is used.')
'markdownDescription': nls.localizeByDefault('Controls the font size in pixels for CodeLens. When set to `0`, the 90% of `#editor.fontSize#` is used.')
},
'editor.colorDecorators': {
'description': nls.localizeByDefault('Controls whether the editor should render the inline color decorators and color picker.'),
Expand Down Expand Up @@ -392,11 +392,11 @@ const codeEditorPreferenceProperties = {
'maximum': 1073741824
},
'editor.cursorSurroundingLinesStyle': {
'enumDescriptions': [
'markdownEnumDescriptions': [
nls.localizeByDefault('`cursorSurroundingLines` is enforced only when triggered via the keyboard or API.'),
nls.localizeByDefault('`cursorSurroundingLines` is enforced always.')
],
'description': nls.localizeByDefault('Controls when `cursorSurroundingLines` should be enforced.'),
'markdownDescription': nls.localizeByDefault('Controls when `cursorSurroundingLines` should be enforced.'),
'type': 'string',
'enum': [
'default',
Expand Down Expand Up @@ -1111,7 +1111,7 @@ const codeEditorPreferenceProperties = {
'editor.inlineHints.fontSize': {
'type': 'number',
'default': EDITOR_FONT_DEFAULTS.fontSize,
description: nls.localizeByDefault('Controls font size of inline hints in the editor. When set to `0`, the 90% of `#editor.fontSize#` is used.')
markdownDescription: nls.localizeByDefault('Controls font size of inline hints in the editor. When set to `0`, the 90% of `#editor.fontSize#` is used.')
},
'editor.inlineHints.fontFamily': {
'type': 'string',
Expand Down Expand Up @@ -1173,7 +1173,7 @@ const codeEditorPreferenceProperties = {
'editor.suggest.insertHighlight': {
'type': 'boolean',
'default': false,
'description': nls.localize('theia/editor/suggest.insertHighlight', 'Controls whether unexpected text modifications while accepting completions should be highlighted, e.g `insertMode` is `replace` but the completion only supports `insert`.')
'markdownDescription': nls.localize('theia/editor/suggest.insertHighlight', 'Controls whether unexpected text modifications while accepting completions should be highlighted, e.g `insertMode` is `replace` but the completion only supports `insert`.')
},
'editor.suggest.filterGraceful': {
'type': 'boolean',
Expand Down
4 changes: 2 additions & 2 deletions packages/filesystem/src/browser/filesystem-preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export const filesystemPreferenceSchema: PreferenceSchema = {
type: 'object',
default: { '**/.git': true, '**/.svn': true, '**/.hg': true, '**/CVS': true, '**/.DS_Store': true },
// eslint-disable-next-line max-len
description: nls.localize('theia/filesystem/filesExclude', 'Configure glob patterns for excluding files and folders. For example, the file Explorer decides which files and folders to show or hide based on this setting. Refer to the `#search.exclude#` setting to define search specific excludes.'),
markdownDescription: nls.localize('theia/filesystem/filesExclude', 'Configure glob patterns for excluding files and folders. For example, the file Explorer decides which files and folders to show or hide based on this setting.'),
scope: 'resource'
},
'files.enableTrash': {
Expand All @@ -65,7 +65,7 @@ export const filesystemPreferenceSchema: PreferenceSchema = {
},
'files.associations': {
type: 'object',
description: nls.localizeByDefault(
markdownDescription: nls.localizeByDefault(
'Configure file associations to languages (e.g. `\"*.extension\": \"html\"`). These have precedence over the default associations of the languages installed.'
)
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export const HostedPluginConfigSchema: PreferenceSchema = {
items: {
type: 'string'
},
description: nls.localize(
markdownDescription: nls.localize(
'theia/plugin-dev/launchOutFiles',
'Array of glob patterns for locating generated JavaScript files (`${pluginPath}` will be replaced by plugin actual path).'
),
Expand Down
5 changes: 5 additions & 0 deletions packages/preferences/src/browser/preference-tree-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,11 @@ export class PreferenceTreeModel extends TreeModelImpl {
}
}

getNodeFromPreferenceId(id: string): Preference.TreeNode | undefined {
const node = this.getNode(this.treeGenerator.getNodeId(id));
return node && Preference.TreeNode.is(node) ? node : undefined;
}

/**
* @returns true if selection changed, false otherwise
*/
Expand Down
5 changes: 5 additions & 0 deletions packages/preferences/src/browser/style/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,11 @@
line-height: 18px;
}

.theia-settings-container .pref-description a {
text-decoration-line: none;
cursor: pointer;
}

.theia-settings-container .theia-select:focus {
outline-width: 1px;
outline-style: solid;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,12 @@ export class PreferenceTreeGenerator {
return root;
};

getNodeId(preferenceId: string): string {
const expectedGroup = this.getGroupName(preferenceId.split('.'));
const expectedId = `${expectedGroup}@${preferenceId}`;
return expectedId;
}

protected getGroupName(labels: string[]): string {
const defaultGroup = labels[0];
if (this.topLevelCategories.has(defaultGroup)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ describe('preference-tree-label-provider', () => {
visible: true,
selected: false,
depth: 2,
preferenceId: property,
preference: { data: {} }
};

Expand Down
1 change: 1 addition & 0 deletions packages/preferences/src/browser/util/preference-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export namespace Preference {
export interface LeafNode extends BaseTreeNode {
depth: number;
preference: { data: PreferenceDataProperty };
preferenceId: string;
}

export namespace LeafNode {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import { PreferenceLeafNodeRenderer } from './preference-node-renderer';
import { injectable, inject } from '@theia/core/shared/inversify';
import { CommandService } from '@theia/core/lib/common';
import { CommandService, nls } from '@theia/core/lib/common';
import { PreferencesCommands } from '../../util/preference-types';
import { JSONValue } from '@theia/core/shared/@phosphor/coreutils';

Expand All @@ -25,7 +25,7 @@ export class PreferenceJSONLinkRenderer extends PreferenceLeafNodeRenderer<JSONV
@inject(CommandService) protected readonly commandService: CommandService;

protected createInteractable(parent: HTMLElement): void {
const message = 'Edit in settings.json';
const message = nls.localizeByDefault('Edit in settings.json');
const interactable = document.createElement('a');
this.interactable = interactable;
interactable.classList.add('theia-json-input');
Expand Down
Loading

0 comments on commit 1bdafc4

Please sign in to comment.