@theia/core 1.67.0-next.3 → 1.67.0-next.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -12
- package/lib/browser/authentication-service.d.ts +1 -0
- package/lib/browser/authentication-service.d.ts.map +1 -1
- package/lib/browser/authentication-service.js.map +1 -1
- package/lib/browser/badges/badge-service.d.ts +14 -0
- package/lib/browser/badges/badge-service.d.ts.map +1 -0
- package/lib/browser/badges/badge-service.js +45 -0
- package/lib/browser/badges/badge-service.js.map +1 -0
- package/lib/browser/badges/index.d.ts +3 -0
- package/lib/browser/badges/index.d.ts.map +1 -0
- package/lib/browser/badges/index.js +21 -0
- package/lib/browser/badges/index.js.map +1 -0
- package/lib/browser/badges/tabbar-badge-decorator.d.ts +14 -0
- package/lib/browser/badges/tabbar-badge-decorator.d.ts.map +1 -0
- package/lib/browser/badges/tabbar-badge-decorator.js +68 -0
- package/lib/browser/badges/tabbar-badge-decorator.js.map +1 -0
- package/lib/browser/catalog.json +57 -42
- package/lib/browser/context-key-service.d.ts +4 -1
- package/lib/browser/context-key-service.d.ts.map +1 -1
- package/lib/browser/context-key-service.js +1 -0
- package/lib/browser/context-key-service.js.map +1 -1
- package/lib/browser/dialogs.d.ts.map +1 -1
- package/lib/browser/dialogs.js +2 -1
- package/lib/browser/dialogs.js.map +1 -1
- package/lib/browser/frontend-application-module.d.ts.map +1 -1
- package/lib/browser/frontend-application-module.js +2 -0
- package/lib/browser/frontend-application-module.js.map +1 -1
- package/lib/browser/index.d.ts +3 -0
- package/lib/browser/index.d.ts.map +1 -1
- package/lib/browser/index.js +3 -0
- package/lib/browser/index.js.map +1 -1
- package/lib/browser/markdown-rendering/markdown-renderer.d.ts.map +1 -1
- package/lib/browser/markdown-rendering/markdown-renderer.js +2 -1
- package/lib/browser/markdown-rendering/markdown-renderer.js.map +1 -1
- package/lib/browser/markdown-rendering/markdown.d.ts +127 -0
- package/lib/browser/markdown-rendering/markdown.d.ts.map +1 -0
- package/lib/browser/markdown-rendering/markdown.js +188 -0
- package/lib/browser/markdown-rendering/markdown.js.map +1 -0
- package/lib/browser/markdown-rendering/markdown.spec.d.ts +2 -0
- package/lib/browser/markdown-rendering/markdown.spec.d.ts.map +1 -0
- package/lib/browser/markdown-rendering/markdown.spec.js +243 -0
- package/lib/browser/markdown-rendering/markdown.spec.js.map +1 -0
- package/lib/browser/shell/tab-bars.d.ts +1 -1
- package/lib/browser/shell/tab-bars.d.ts.map +1 -1
- package/lib/browser/shell/tab-bars.js +52 -49
- package/lib/browser/shell/tab-bars.js.map +1 -1
- package/lib/browser/widgets/select-component.d.ts +1 -1
- package/lib/browser/widgets/select-component.d.ts.map +1 -1
- package/lib/browser/widgets/select-component.js +6 -2
- package/lib/browser/widgets/select-component.js.map +1 -1
- package/lib/browser/widgets/widget.d.ts +1 -0
- package/lib/browser/widgets/widget.d.ts.map +1 -1
- package/lib/browser/widgets/widget.js +5 -0
- package/lib/browser/widgets/widget.js.map +1 -1
- package/lib/common/markdown-rendering/markdown-string.d.ts +1 -1
- package/lib/common/markdown-rendering/markdown-string.d.ts.map +1 -1
- package/lib/common/markdown-rendering/markdown-string.js.map +1 -1
- package/lib/common/theme.d.ts +2 -0
- package/lib/common/theme.d.ts.map +1 -1
- package/lib/common/theme.js +14 -1
- package/lib/common/theme.js.map +1 -1
- package/package.json +21 -16
- package/shared/markdown-it-anchor/index.d.ts +2 -0
- package/shared/markdown-it-anchor/index.js +1 -0
- package/shared/markdown-it-emoji/index.d.ts +2 -0
- package/shared/markdown-it-emoji/index.js +1 -0
- package/src/browser/authentication-service.ts +1 -0
- package/src/browser/badges/badge-service.ts +44 -0
- package/src/browser/badges/index.ts +18 -0
- package/src/browser/badges/tabbar-badge-decorator.ts +63 -0
- package/src/browser/context-key-service.ts +5 -1
- package/src/browser/dialogs.ts +3 -2
- package/src/browser/frontend-application-module.ts +2 -0
- package/src/browser/index.ts +3 -0
- package/src/browser/markdown-rendering/markdown-renderer.ts +2 -1
- package/src/browser/markdown-rendering/markdown.spec.tsx +371 -0
- package/src/browser/markdown-rendering/markdown.tsx +282 -0
- package/src/browser/shell/tab-bars.ts +24 -26
- package/src/browser/widgets/select-component.tsx +6 -2
- package/src/browser/widgets/widget.ts +6 -0
- package/src/common/markdown-rendering/markdown-string.ts +1 -1
- package/src/common/theme.ts +13 -0
package/package.json
CHANGED
|
@@ -1,30 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theia/core",
|
|
3
|
-
"version": "1.67.0-next.
|
|
3
|
+
"version": "1.67.0-next.59+3f14297ea",
|
|
4
4
|
"description": "Theia is a cloud & desktop IDE framework implemented in TypeScript.",
|
|
5
5
|
"main": "lib/common/index.js",
|
|
6
6
|
"typings": "lib/common/index.d.ts",
|
|
7
7
|
"dependencies": {
|
|
8
8
|
"@babel/runtime": "^7.10.0",
|
|
9
|
-
"@lumino/algorithm": "^2.0.
|
|
10
|
-
"@lumino/commands": "^2.3.
|
|
11
|
-
"@lumino/coreutils": "^2.2.
|
|
12
|
-
"@lumino/domutils": "^2.0.
|
|
13
|
-
"@lumino/dragdrop": "^2.1.
|
|
14
|
-
"@lumino/messaging": "^2.0.
|
|
15
|
-
"@lumino/properties": "^2.0.
|
|
16
|
-
"@lumino/signaling": "^2.1.
|
|
17
|
-
"@lumino/virtualdom": "^2.0.
|
|
18
|
-
"@lumino/widgets": "2.
|
|
9
|
+
"@lumino/algorithm": "^2.0.4",
|
|
10
|
+
"@lumino/commands": "^2.3.3",
|
|
11
|
+
"@lumino/coreutils": "^2.2.2",
|
|
12
|
+
"@lumino/domutils": "^2.0.4",
|
|
13
|
+
"@lumino/dragdrop": "^2.1.7",
|
|
14
|
+
"@lumino/messaging": "^2.0.4",
|
|
15
|
+
"@lumino/properties": "^2.0.4",
|
|
16
|
+
"@lumino/signaling": "^2.1.5",
|
|
17
|
+
"@lumino/virtualdom": "^2.0.4",
|
|
18
|
+
"@lumino/widgets": "2.7.2",
|
|
19
19
|
"@parcel/watcher": "^2.5.0",
|
|
20
|
-
"@theia/application-package": "1.67.0-next.
|
|
21
|
-
"@theia/request": "1.67.0-next.
|
|
20
|
+
"@theia/application-package": "1.67.0-next.59+3f14297ea",
|
|
21
|
+
"@theia/request": "1.67.0-next.59+3f14297ea",
|
|
22
22
|
"@types/body-parser": "^1.16.4",
|
|
23
23
|
"@types/express": "^4.17.21",
|
|
24
24
|
"@types/fs-extra": "^4.0.2",
|
|
25
25
|
"@types/lodash.debounce": "4.0.3",
|
|
26
26
|
"@types/lodash.throttle": "^4.1.3",
|
|
27
|
-
"@types/markdown-it": "^
|
|
27
|
+
"@types/markdown-it": "^14.1.0",
|
|
28
|
+
"@types/markdown-it-emoji": "^3.0.1",
|
|
28
29
|
"@types/react": "^18.0.15",
|
|
29
30
|
"@types/react-dom": "^18.0.6",
|
|
30
31
|
"@types/route-parser": "^0.1.1",
|
|
@@ -53,7 +54,9 @@
|
|
|
53
54
|
"keytar": "7.9.0",
|
|
54
55
|
"lodash.debounce": "^4.0.8",
|
|
55
56
|
"lodash.throttle": "^4.1.1",
|
|
56
|
-
"markdown-it": "^
|
|
57
|
+
"markdown-it": "^14.1.0",
|
|
58
|
+
"markdown-it-anchor": "^9.2.0",
|
|
59
|
+
"markdown-it-emoji": "^3.0.0",
|
|
57
60
|
"msgpackr": "^1.10.2",
|
|
58
61
|
"p-debounce": "^2.1.0",
|
|
59
62
|
"perfect-scrollbar": "1.5.5",
|
|
@@ -122,6 +125,8 @@
|
|
|
122
125
|
"lodash.debounce as debounce",
|
|
123
126
|
"lodash.throttle as throttle",
|
|
124
127
|
"markdown-it as markdownit",
|
|
128
|
+
"markdown-it-anchor as markdownitanchor",
|
|
129
|
+
"markdown-it-emoji as markdownitemoji",
|
|
125
130
|
"react as React",
|
|
126
131
|
"ws as WebSocket",
|
|
127
132
|
"yargs"
|
|
@@ -216,5 +221,5 @@
|
|
|
216
221
|
"nyc": {
|
|
217
222
|
"extends": "../../configs/nyc.json"
|
|
218
223
|
},
|
|
219
|
-
"gitHead": "
|
|
224
|
+
"gitHead": "3f14297ea2edcdb1fffd74afee0613e70b43e125"
|
|
220
225
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('markdown-it-anchor');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('markdown-it-emoji');
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2025 EclipseSource GmbH and others.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { injectable } from 'inversify';
|
|
18
|
+
import { Emitter, Event } from '../../common';
|
|
19
|
+
import { Widget } from '../widgets';
|
|
20
|
+
|
|
21
|
+
export interface Badge {
|
|
22
|
+
value: number;
|
|
23
|
+
tooltip: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@injectable()
|
|
27
|
+
export class BadgeService {
|
|
28
|
+
protected readonly badges = new WeakMap<Widget, Badge>();
|
|
29
|
+
protected readonly onDidChangeBadgesEmitter = new Emitter<Widget>();
|
|
30
|
+
get onDidChangeBadges(): Event<Widget> { return this.onDidChangeBadgesEmitter.event; }
|
|
31
|
+
|
|
32
|
+
getBadge(widget: Widget): Badge | undefined {
|
|
33
|
+
return this.badges.get(widget);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
showBadge(widget: Widget, badge?: Badge): void {
|
|
37
|
+
if (badge) {
|
|
38
|
+
this.badges.set(widget, badge);
|
|
39
|
+
this.onDidChangeBadgesEmitter.fire(widget);
|
|
40
|
+
} else if (this.badges.delete(widget)) {
|
|
41
|
+
this.onDidChangeBadgesEmitter.fire(widget);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2025 EclipseSource GmbH and others.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
export * from './badge-service';
|
|
18
|
+
export * from './tabbar-badge-decorator';
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2025 EclipseSource GmbH and others.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { inject, injectable, interfaces } from 'inversify';
|
|
18
|
+
import { ViewContainer } from '../view-container';
|
|
19
|
+
import { WidgetDecoration } from '../widget-decoration';
|
|
20
|
+
import { Title, Widget } from '../widgets';
|
|
21
|
+
import { TabBarDecorator } from '../shell/tab-bar-decorator';
|
|
22
|
+
import { Disposable, Event } from '../../common';
|
|
23
|
+
import { Badge, BadgeService } from './badge-service';
|
|
24
|
+
|
|
25
|
+
@injectable()
|
|
26
|
+
export class TabBarBadgeDecorator implements TabBarDecorator {
|
|
27
|
+
readonly id = 'theia-plugin-view-container-badge-decorator';
|
|
28
|
+
|
|
29
|
+
@inject(BadgeService)
|
|
30
|
+
protected readonly badgeService: BadgeService;
|
|
31
|
+
|
|
32
|
+
onDidChangeDecorations(...[cb, thisArg, disposable]: Parameters<Event<void>>): Disposable { return this.badgeService.onDidChangeBadges(() => cb(), thisArg, disposable); }
|
|
33
|
+
|
|
34
|
+
decorate({ owner }: Title<Widget>): WidgetDecoration.Data[] {
|
|
35
|
+
let total = 0;
|
|
36
|
+
const result: WidgetDecoration.Data[] = [];
|
|
37
|
+
const aggregate = (badge?: Badge) => {
|
|
38
|
+
if (badge?.value) {
|
|
39
|
+
total += badge.value;
|
|
40
|
+
}
|
|
41
|
+
if (badge?.tooltip) {
|
|
42
|
+
result.push({ tooltip: badge.tooltip });
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
if (owner instanceof ViewContainer) {
|
|
46
|
+
for (const { wrapped } of owner.getParts()) {
|
|
47
|
+
aggregate(this.badgeService.getBadge(wrapped));
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
aggregate(this.badgeService.getBadge(owner));
|
|
51
|
+
}
|
|
52
|
+
if (total !== 0) {
|
|
53
|
+
result.push({ badge: total });
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function bindBadgeDecoration(bind: interfaces.Bind): void {
|
|
60
|
+
bind(BadgeService).toSelf().inSingletonScope();
|
|
61
|
+
bind(TabBarBadgeDecorator).toSelf().inSingletonScope();
|
|
62
|
+
bind(TabBarDecorator).toService(TabBarBadgeDecorator);
|
|
63
|
+
}
|
|
@@ -89,7 +89,9 @@ export interface ContextKeyService extends ContextMatcher {
|
|
|
89
89
|
setContext(key: string, value: unknown): void;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
export type ScopedValueStore = Omit<ContextKeyService, 'onDidChange' | 'match' | 'parseKeys' | 'with' | 'createOverlay'> & Disposable
|
|
92
|
+
export type ScopedValueStore = Omit<ContextKeyService, 'onDidChange' | 'match' | 'parseKeys' | 'with' | 'createOverlay'> & Disposable & {
|
|
93
|
+
onDidChangeContext: Event<ContextKeyChangeEvent>;
|
|
94
|
+
};
|
|
93
95
|
|
|
94
96
|
@injectable()
|
|
95
97
|
export class ContextKeyServiceDummyImpl implements ContextKeyService {
|
|
@@ -99,6 +101,8 @@ export class ContextKeyServiceDummyImpl implements ContextKeyService {
|
|
|
99
101
|
this.onDidChangeEmitter.fire(event);
|
|
100
102
|
}
|
|
101
103
|
|
|
104
|
+
onDidChangeContext: Event<ContextKeyChangeEvent> = this.onDidChangeEmitter.event;
|
|
105
|
+
|
|
102
106
|
createKey<T extends ContextKeyValue>(key: string, defaultValue: T | undefined): ContextKey<T> {
|
|
103
107
|
return ContextKey.None;
|
|
104
108
|
}
|
package/src/browser/dialogs.ts
CHANGED
|
@@ -248,8 +248,9 @@ export abstract class AbstractDialog<T> extends BaseWidget {
|
|
|
248
248
|
* Please note that this may also include other popups such as the suggestion overlay, the notification center or quick picks.
|
|
249
249
|
* @returns a disposable that will restore the previous tabbing behavior
|
|
250
250
|
*/
|
|
251
|
-
protected preventTabbingOutsideDialog(elements = Array.from(this.node.ownerDocument.body.children)): Disposable {
|
|
252
|
-
const
|
|
251
|
+
protected preventTabbingOutsideDialog(elements = Array.from(this.node.ownerDocument.body.children)): Disposable { //
|
|
252
|
+
const inertBlacklist = ['select-component-container']; // IDs of elements that should remain interactive
|
|
253
|
+
const nonInertElements = elements.filter(child => child !== this.node && !(child.hasAttribute('inert')) && !inertBlacklist.includes(child.id));
|
|
253
254
|
nonInertElements.forEach(child => child.setAttribute('inert', ''));
|
|
254
255
|
return Disposable.create(() => nonInertElements.forEach(child => child.removeAttribute('inert')));
|
|
255
256
|
}
|
|
@@ -143,6 +143,7 @@ import { DomInputUndoRedoHandler, UndoRedoHandler, UndoRedoHandlerService } from
|
|
|
143
143
|
import { WidgetStatusBarContribution, WidgetStatusBarService } from './widget-status-bar-service';
|
|
144
144
|
import { SymbolIconColorContribution } from './symbol-icon-color-contribution';
|
|
145
145
|
import { CorePreferences, bindCorePreferences } from '../common/core-preferences';
|
|
146
|
+
import { bindBadgeDecoration } from './badges';
|
|
146
147
|
|
|
147
148
|
export { bindResourceProvider, bindMessageService, bindPreferenceService };
|
|
148
149
|
|
|
@@ -481,4 +482,5 @@ export const frontendApplicationModule = new ContainerModule((bind, _unbind, _is
|
|
|
481
482
|
bind(WidgetStatusBarService).toSelf().inSingletonScope();
|
|
482
483
|
bind(FrontendApplicationContribution).toService(WidgetStatusBarService);
|
|
483
484
|
bindContributionProvider(bind, WidgetStatusBarContribution);
|
|
485
|
+
bindBadgeDecoration(bind);
|
|
484
486
|
});
|
package/src/browser/index.ts
CHANGED
|
@@ -50,3 +50,6 @@ export * from './hover-service';
|
|
|
50
50
|
export * from './saveable-service';
|
|
51
51
|
export * from './undo-redo-handler';
|
|
52
52
|
export * from './widget-status-bar-service';
|
|
53
|
+
export * from './badges';
|
|
54
|
+
export * from './markdown-rendering/markdown-renderer';
|
|
55
|
+
export * from './markdown-rendering/markdown';
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import * as DOMPurify from 'dompurify';
|
|
18
18
|
import { injectable, inject, postConstruct } from 'inversify';
|
|
19
19
|
import * as markdownit from 'markdown-it';
|
|
20
|
+
import * as markdownitemoji from 'markdown-it-emoji';
|
|
20
21
|
import { MarkdownString } from '../../common/markdown-rendering/markdown-string';
|
|
21
22
|
import { Disposable, DisposableGroup } from '../../common';
|
|
22
23
|
import { LabelParser } from '../label-parser';
|
|
@@ -65,7 +66,7 @@ export interface MarkdownRendererFactory {
|
|
|
65
66
|
@injectable()
|
|
66
67
|
export class MarkdownRendererImpl implements MarkdownRenderer {
|
|
67
68
|
@inject(LabelParser) protected readonly labelParser: LabelParser;
|
|
68
|
-
protected readonly markdownIt = markdownit();
|
|
69
|
+
protected readonly markdownIt = markdownit().use(markdownitemoji.full);
|
|
69
70
|
protected resetRenderer: Disposable | undefined;
|
|
70
71
|
|
|
71
72
|
@postConstruct()
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2025 EclipseSource and others.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import * as assert from 'assert';
|
|
18
|
+
import * as React from 'react';
|
|
19
|
+
import { createRoot, Root } from 'react-dom/client';
|
|
20
|
+
import { enableJSDOM } from '../test/jsdom';
|
|
21
|
+
import { Markdown, LocalizedMarkdown } from './markdown';
|
|
22
|
+
import { MarkdownRenderer } from './markdown-renderer';
|
|
23
|
+
import { MarkdownString, MarkdownStringImpl } from '../../common/markdown-rendering/markdown-string';
|
|
24
|
+
|
|
25
|
+
let disableJSDOM: () => void;
|
|
26
|
+
|
|
27
|
+
describe('Markdown', () => {
|
|
28
|
+
let mockRenderer: MarkdownRenderer & { lastRenderedMarkdown?: MarkdownString };
|
|
29
|
+
let container: HTMLElement;
|
|
30
|
+
let root: Root;
|
|
31
|
+
|
|
32
|
+
before(() => disableJSDOM = enableJSDOM());
|
|
33
|
+
after(() => disableJSDOM());
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
container = document.createElement('div');
|
|
37
|
+
document.body.appendChild(container);
|
|
38
|
+
root = createRoot(container);
|
|
39
|
+
|
|
40
|
+
mockRenderer = {
|
|
41
|
+
lastRenderedMarkdown: undefined,
|
|
42
|
+
render: (markdown: MarkdownString | undefined) => {
|
|
43
|
+
// Store the markdown for verification
|
|
44
|
+
if (typeof markdown === 'object' && 'value' in markdown) {
|
|
45
|
+
mockRenderer.lastRenderedMarkdown = markdown;
|
|
46
|
+
}
|
|
47
|
+
const div = document.createElement('div');
|
|
48
|
+
if (markdown) {
|
|
49
|
+
const p = document.createElement('p');
|
|
50
|
+
const value = typeof markdown === 'object' && 'value' in markdown
|
|
51
|
+
? markdown.value
|
|
52
|
+
: String(markdown);
|
|
53
|
+
p.textContent = value;
|
|
54
|
+
div.appendChild(p);
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
element: div,
|
|
58
|
+
dispose: () => { }
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
afterEach(() => {
|
|
65
|
+
root.unmount();
|
|
66
|
+
document.body.removeChild(container);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should render markdown content', done => {
|
|
70
|
+
root.render(
|
|
71
|
+
<Markdown
|
|
72
|
+
markdown="**Hello World**"
|
|
73
|
+
markdownRenderer={mockRenderer}
|
|
74
|
+
className="test-class"
|
|
75
|
+
/>
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
setTimeout(() => {
|
|
79
|
+
const div = container.querySelector('.test-class');
|
|
80
|
+
assert.ok(div, 'Container should exist');
|
|
81
|
+
assert.ok(div?.textContent?.includes('Hello World'), 'Should contain markdown text');
|
|
82
|
+
done();
|
|
83
|
+
}, 50);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should render empty div when markdown is undefined', done => {
|
|
87
|
+
root.render(
|
|
88
|
+
<Markdown
|
|
89
|
+
markdown={undefined}
|
|
90
|
+
markdownRenderer={mockRenderer}
|
|
91
|
+
className="test-class"
|
|
92
|
+
/>
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
setTimeout(() => {
|
|
96
|
+
const div = container.querySelector('.test-class');
|
|
97
|
+
assert.ok(div, 'Container should exist');
|
|
98
|
+
assert.strictEqual(div?.childNodes.length, 0, 'Should have no children');
|
|
99
|
+
done();
|
|
100
|
+
}, 50);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should render empty div when markdown is empty string', done => {
|
|
104
|
+
root.render(
|
|
105
|
+
<Markdown
|
|
106
|
+
markdown=""
|
|
107
|
+
markdownRenderer={mockRenderer}
|
|
108
|
+
className="test-class"
|
|
109
|
+
/>
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
setTimeout(() => {
|
|
113
|
+
const div = container.querySelector('.test-class');
|
|
114
|
+
assert.ok(div, 'Container should exist');
|
|
115
|
+
assert.strictEqual(div?.childNodes.length, 0, 'Should have no children');
|
|
116
|
+
done();
|
|
117
|
+
}, 50);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should render empty div when markdown is whitespace only', done => {
|
|
121
|
+
root.render(
|
|
122
|
+
<Markdown
|
|
123
|
+
markdown=" "
|
|
124
|
+
markdownRenderer={mockRenderer}
|
|
125
|
+
className="test-class"
|
|
126
|
+
/>
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
setTimeout(() => {
|
|
130
|
+
const div = container.querySelector('.test-class');
|
|
131
|
+
assert.ok(div, 'Container should exist');
|
|
132
|
+
assert.strictEqual(div?.childNodes.length, 0, 'Should have no children');
|
|
133
|
+
done();
|
|
134
|
+
}, 50);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should accept MarkdownString object', done => {
|
|
138
|
+
const markdownString = new MarkdownStringImpl('**Bold Text**');
|
|
139
|
+
root.render(
|
|
140
|
+
<Markdown
|
|
141
|
+
markdown={markdownString}
|
|
142
|
+
markdownRenderer={mockRenderer}
|
|
143
|
+
className="test-class"
|
|
144
|
+
/>
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
setTimeout(() => {
|
|
148
|
+
const div = container.querySelector('.test-class');
|
|
149
|
+
assert.ok(div, 'Container should exist');
|
|
150
|
+
assert.ok(div?.textContent?.includes('Bold Text'), 'Should contain markdown text');
|
|
151
|
+
done();
|
|
152
|
+
}, 50);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should call onRender callback when content is rendered', done => {
|
|
156
|
+
let renderCallbackCalled = false;
|
|
157
|
+
let receivedElement: HTMLElement | undefined;
|
|
158
|
+
|
|
159
|
+
root.render(
|
|
160
|
+
<Markdown
|
|
161
|
+
markdown="Test content"
|
|
162
|
+
markdownRenderer={mockRenderer}
|
|
163
|
+
className="test-class"
|
|
164
|
+
onRender={element => {
|
|
165
|
+
renderCallbackCalled = true;
|
|
166
|
+
receivedElement = element;
|
|
167
|
+
}}
|
|
168
|
+
/>
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
setTimeout(() => {
|
|
172
|
+
assert.ok(renderCallbackCalled, 'onRender should be called');
|
|
173
|
+
assert.ok(receivedElement, 'Should receive element');
|
|
174
|
+
done();
|
|
175
|
+
}, 50);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should call onRender callback with undefined when content is empty', done => {
|
|
179
|
+
let renderCallbackCalled = false;
|
|
180
|
+
let receivedElement: HTMLElement | undefined = document.createElement('div'); // Initialize to non-undefined
|
|
181
|
+
|
|
182
|
+
root.render(
|
|
183
|
+
<Markdown
|
|
184
|
+
markdown={undefined}
|
|
185
|
+
markdownRenderer={mockRenderer}
|
|
186
|
+
className="test-class"
|
|
187
|
+
onRender={element => {
|
|
188
|
+
renderCallbackCalled = true;
|
|
189
|
+
receivedElement = element;
|
|
190
|
+
}}
|
|
191
|
+
/>
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
setTimeout(() => {
|
|
195
|
+
assert.ok(renderCallbackCalled, 'onRender should be called even for empty content');
|
|
196
|
+
assert.strictEqual(receivedElement, undefined, 'Should receive undefined for empty content');
|
|
197
|
+
done();
|
|
198
|
+
}, 100);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('should apply className to container', done => {
|
|
202
|
+
root.render(
|
|
203
|
+
<Markdown
|
|
204
|
+
markdown="Test"
|
|
205
|
+
markdownRenderer={mockRenderer}
|
|
206
|
+
className="custom-class"
|
|
207
|
+
/>
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
setTimeout(() => {
|
|
211
|
+
const div = container.querySelector('.custom-class');
|
|
212
|
+
assert.ok(div, 'Container with custom class should exist');
|
|
213
|
+
done();
|
|
214
|
+
}, 50);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe('LocalizedMarkdown', () => {
|
|
219
|
+
let mockRenderer: MarkdownRenderer & { lastRenderedMarkdown?: MarkdownString };
|
|
220
|
+
let container: HTMLElement;
|
|
221
|
+
let root: Root;
|
|
222
|
+
|
|
223
|
+
before(() => disableJSDOM = enableJSDOM());
|
|
224
|
+
after(() => disableJSDOM());
|
|
225
|
+
|
|
226
|
+
beforeEach(() => {
|
|
227
|
+
container = document.createElement('div');
|
|
228
|
+
document.body.appendChild(container);
|
|
229
|
+
root = createRoot(container);
|
|
230
|
+
|
|
231
|
+
mockRenderer = {
|
|
232
|
+
lastRenderedMarkdown: undefined,
|
|
233
|
+
render: (markdown: MarkdownString | undefined) => {
|
|
234
|
+
// Store the markdown for verification
|
|
235
|
+
if (typeof markdown === 'object' && 'value' in markdown) {
|
|
236
|
+
mockRenderer.lastRenderedMarkdown = markdown;
|
|
237
|
+
}
|
|
238
|
+
const div = document.createElement('div');
|
|
239
|
+
if (markdown) {
|
|
240
|
+
const p = document.createElement('p');
|
|
241
|
+
const value = typeof markdown === 'object' && 'value' in markdown
|
|
242
|
+
? markdown.value
|
|
243
|
+
: String(markdown);
|
|
244
|
+
p.textContent = value;
|
|
245
|
+
div.appendChild(p);
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
element: div,
|
|
249
|
+
dispose: () => { }
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
afterEach(() => {
|
|
256
|
+
root.unmount();
|
|
257
|
+
document.body.removeChild(container);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should render localized markdown content', done => {
|
|
261
|
+
root.render(
|
|
262
|
+
<LocalizedMarkdown
|
|
263
|
+
localizationKey="test/basic"
|
|
264
|
+
defaultMarkdown="Welcome to **Theia**!"
|
|
265
|
+
markdownRenderer={mockRenderer}
|
|
266
|
+
className="test-class"
|
|
267
|
+
/>
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
setTimeout(() => {
|
|
271
|
+
const div = container.querySelector('.test-class');
|
|
272
|
+
assert.ok(div, 'Container should exist');
|
|
273
|
+
// The content should be localized (though in tests it will be the default)
|
|
274
|
+
assert.ok(div?.textContent?.includes('Theia'), 'Should contain localized text');
|
|
275
|
+
done();
|
|
276
|
+
}, 50);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('should render localized markdown with parameters', done => {
|
|
280
|
+
root.render(
|
|
281
|
+
<LocalizedMarkdown
|
|
282
|
+
localizationKey="test/greeting"
|
|
283
|
+
defaultMarkdown="Hello **{0}**! You have {1} messages."
|
|
284
|
+
args={['Alice', 5]}
|
|
285
|
+
markdownRenderer={mockRenderer}
|
|
286
|
+
className="test-class"
|
|
287
|
+
/>
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
setTimeout(() => {
|
|
291
|
+
const div = container.querySelector('.test-class');
|
|
292
|
+
assert.ok(div, 'Container should exist');
|
|
293
|
+
assert.ok(div?.textContent?.includes('Alice'), 'Should contain first parameter');
|
|
294
|
+
assert.ok(div?.textContent?.includes('5'), 'Should contain second parameter');
|
|
295
|
+
done();
|
|
296
|
+
}, 50);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('should render empty div when default markdown is empty', done => {
|
|
300
|
+
root.render(
|
|
301
|
+
<LocalizedMarkdown
|
|
302
|
+
localizationKey="test/empty"
|
|
303
|
+
defaultMarkdown=""
|
|
304
|
+
markdownRenderer={mockRenderer}
|
|
305
|
+
className="test-class"
|
|
306
|
+
/>
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
setTimeout(() => {
|
|
310
|
+
const div = container.querySelector('.test-class');
|
|
311
|
+
assert.ok(div, 'Container should exist');
|
|
312
|
+
assert.strictEqual(div?.childNodes.length, 0, 'Should have no children');
|
|
313
|
+
done();
|
|
314
|
+
}, 50);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('should update when localization key changes', done => {
|
|
318
|
+
const { rerender } = { rerender: (element: React.ReactElement) => root.render(element) };
|
|
319
|
+
|
|
320
|
+
root.render(
|
|
321
|
+
<LocalizedMarkdown
|
|
322
|
+
localizationKey="test/first"
|
|
323
|
+
defaultMarkdown="First content"
|
|
324
|
+
markdownRenderer={mockRenderer}
|
|
325
|
+
className="test-class"
|
|
326
|
+
/>
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
setTimeout(() => {
|
|
330
|
+
const div = container.querySelector('.test-class');
|
|
331
|
+
assert.ok(div?.textContent?.includes('First'), 'Should contain first content');
|
|
332
|
+
|
|
333
|
+
rerender(
|
|
334
|
+
<LocalizedMarkdown
|
|
335
|
+
localizationKey="test/second"
|
|
336
|
+
defaultMarkdown="Second content"
|
|
337
|
+
markdownRenderer={mockRenderer}
|
|
338
|
+
className="test-class"
|
|
339
|
+
/>
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
setTimeout(() => {
|
|
343
|
+
const updatedDiv = container.querySelector('.test-class');
|
|
344
|
+
assert.ok(updatedDiv?.textContent?.includes('Second'), 'Should contain second content');
|
|
345
|
+
done();
|
|
346
|
+
}, 50);
|
|
347
|
+
}, 50);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('should pass markdown options correctly', done => {
|
|
351
|
+
root.render(
|
|
352
|
+
<LocalizedMarkdown
|
|
353
|
+
localizationKey="test/html"
|
|
354
|
+
defaultMarkdown="Content with <span>HTML</span>"
|
|
355
|
+
markdownRenderer={mockRenderer}
|
|
356
|
+
className="test-class"
|
|
357
|
+
markdownOptions={{ supportHtml: true, supportThemeIcons: true, isTrusted: true }}
|
|
358
|
+
/>
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
setTimeout(() => {
|
|
362
|
+
const div = container.querySelector('.test-class');
|
|
363
|
+
assert.ok(div, 'Container should exist');
|
|
364
|
+
assert.ok(mockRenderer.lastRenderedMarkdown, 'Should have rendered markdown');
|
|
365
|
+
assert.strictEqual(mockRenderer.lastRenderedMarkdown?.supportHtml, true, 'Should pass supportHtml option');
|
|
366
|
+
assert.strictEqual(mockRenderer.lastRenderedMarkdown?.supportThemeIcons, true, 'Should pass supportThemeIcons option');
|
|
367
|
+
assert.strictEqual(mockRenderer.lastRenderedMarkdown?.isTrusted, true, 'Should pass isTrusted option');
|
|
368
|
+
done();
|
|
369
|
+
}, 50);
|
|
370
|
+
});
|
|
371
|
+
});
|