@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.
Files changed (82) hide show
  1. package/README.md +14 -12
  2. package/lib/browser/authentication-service.d.ts +1 -0
  3. package/lib/browser/authentication-service.d.ts.map +1 -1
  4. package/lib/browser/authentication-service.js.map +1 -1
  5. package/lib/browser/badges/badge-service.d.ts +14 -0
  6. package/lib/browser/badges/badge-service.d.ts.map +1 -0
  7. package/lib/browser/badges/badge-service.js +45 -0
  8. package/lib/browser/badges/badge-service.js.map +1 -0
  9. package/lib/browser/badges/index.d.ts +3 -0
  10. package/lib/browser/badges/index.d.ts.map +1 -0
  11. package/lib/browser/badges/index.js +21 -0
  12. package/lib/browser/badges/index.js.map +1 -0
  13. package/lib/browser/badges/tabbar-badge-decorator.d.ts +14 -0
  14. package/lib/browser/badges/tabbar-badge-decorator.d.ts.map +1 -0
  15. package/lib/browser/badges/tabbar-badge-decorator.js +68 -0
  16. package/lib/browser/badges/tabbar-badge-decorator.js.map +1 -0
  17. package/lib/browser/catalog.json +57 -42
  18. package/lib/browser/context-key-service.d.ts +4 -1
  19. package/lib/browser/context-key-service.d.ts.map +1 -1
  20. package/lib/browser/context-key-service.js +1 -0
  21. package/lib/browser/context-key-service.js.map +1 -1
  22. package/lib/browser/dialogs.d.ts.map +1 -1
  23. package/lib/browser/dialogs.js +2 -1
  24. package/lib/browser/dialogs.js.map +1 -1
  25. package/lib/browser/frontend-application-module.d.ts.map +1 -1
  26. package/lib/browser/frontend-application-module.js +2 -0
  27. package/lib/browser/frontend-application-module.js.map +1 -1
  28. package/lib/browser/index.d.ts +3 -0
  29. package/lib/browser/index.d.ts.map +1 -1
  30. package/lib/browser/index.js +3 -0
  31. package/lib/browser/index.js.map +1 -1
  32. package/lib/browser/markdown-rendering/markdown-renderer.d.ts.map +1 -1
  33. package/lib/browser/markdown-rendering/markdown-renderer.js +2 -1
  34. package/lib/browser/markdown-rendering/markdown-renderer.js.map +1 -1
  35. package/lib/browser/markdown-rendering/markdown.d.ts +127 -0
  36. package/lib/browser/markdown-rendering/markdown.d.ts.map +1 -0
  37. package/lib/browser/markdown-rendering/markdown.js +188 -0
  38. package/lib/browser/markdown-rendering/markdown.js.map +1 -0
  39. package/lib/browser/markdown-rendering/markdown.spec.d.ts +2 -0
  40. package/lib/browser/markdown-rendering/markdown.spec.d.ts.map +1 -0
  41. package/lib/browser/markdown-rendering/markdown.spec.js +243 -0
  42. package/lib/browser/markdown-rendering/markdown.spec.js.map +1 -0
  43. package/lib/browser/shell/tab-bars.d.ts +1 -1
  44. package/lib/browser/shell/tab-bars.d.ts.map +1 -1
  45. package/lib/browser/shell/tab-bars.js +52 -49
  46. package/lib/browser/shell/tab-bars.js.map +1 -1
  47. package/lib/browser/widgets/select-component.d.ts +1 -1
  48. package/lib/browser/widgets/select-component.d.ts.map +1 -1
  49. package/lib/browser/widgets/select-component.js +6 -2
  50. package/lib/browser/widgets/select-component.js.map +1 -1
  51. package/lib/browser/widgets/widget.d.ts +1 -0
  52. package/lib/browser/widgets/widget.d.ts.map +1 -1
  53. package/lib/browser/widgets/widget.js +5 -0
  54. package/lib/browser/widgets/widget.js.map +1 -1
  55. package/lib/common/markdown-rendering/markdown-string.d.ts +1 -1
  56. package/lib/common/markdown-rendering/markdown-string.d.ts.map +1 -1
  57. package/lib/common/markdown-rendering/markdown-string.js.map +1 -1
  58. package/lib/common/theme.d.ts +2 -0
  59. package/lib/common/theme.d.ts.map +1 -1
  60. package/lib/common/theme.js +14 -1
  61. package/lib/common/theme.js.map +1 -1
  62. package/package.json +21 -16
  63. package/shared/markdown-it-anchor/index.d.ts +2 -0
  64. package/shared/markdown-it-anchor/index.js +1 -0
  65. package/shared/markdown-it-emoji/index.d.ts +2 -0
  66. package/shared/markdown-it-emoji/index.js +1 -0
  67. package/src/browser/authentication-service.ts +1 -0
  68. package/src/browser/badges/badge-service.ts +44 -0
  69. package/src/browser/badges/index.ts +18 -0
  70. package/src/browser/badges/tabbar-badge-decorator.ts +63 -0
  71. package/src/browser/context-key-service.ts +5 -1
  72. package/src/browser/dialogs.ts +3 -2
  73. package/src/browser/frontend-application-module.ts +2 -0
  74. package/src/browser/index.ts +3 -0
  75. package/src/browser/markdown-rendering/markdown-renderer.ts +2 -1
  76. package/src/browser/markdown-rendering/markdown.spec.tsx +371 -0
  77. package/src/browser/markdown-rendering/markdown.tsx +282 -0
  78. package/src/browser/shell/tab-bars.ts +24 -26
  79. package/src/browser/widgets/select-component.tsx +6 -2
  80. package/src/browser/widgets/widget.ts +6 -0
  81. package/src/common/markdown-rendering/markdown-string.ts +1 -1
  82. 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+7946ca874",
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.2",
10
- "@lumino/commands": "^2.3.1",
11
- "@lumino/coreutils": "^2.2.0",
12
- "@lumino/domutils": "^2.0.2",
13
- "@lumino/dragdrop": "^2.1.5",
14
- "@lumino/messaging": "^2.0.2",
15
- "@lumino/properties": "^2.0.2",
16
- "@lumino/signaling": "^2.1.3",
17
- "@lumino/virtualdom": "^2.0.2",
18
- "@lumino/widgets": "2.5.0",
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.3+7946ca874",
21
- "@theia/request": "1.67.0-next.3+7946ca874",
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": "^12.2.3",
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": "^12.3.2",
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": "7946ca874b04e6249d883e9a586f193194df1365"
224
+ "gitHead": "3f14297ea2edcdb1fffd74afee0613e70b43e125"
220
225
  }
@@ -0,0 +1,2 @@
1
+ import markdownitanchor = require('markdown-it-anchor');
2
+ export = markdownitanchor;
@@ -0,0 +1 @@
1
+ module.exports = require('markdown-it-anchor');
@@ -0,0 +1,2 @@
1
+ import markdownitemoji = require('markdown-it-emoji');
2
+ export = markdownitemoji;
@@ -0,0 +1 @@
1
+ module.exports = require('markdown-it-emoji');
@@ -43,6 +43,7 @@ export interface AuthenticationProviderSessionOptions {
43
43
  export interface AuthenticationSession {
44
44
  id: string;
45
45
  accessToken: string;
46
+ idToken?: string;
46
47
  account: AuthenticationSessionAccountInformation;
47
48
  scopes: ReadonlyArray<string>;
48
49
  }
@@ -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
  }
@@ -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 nonInertElements = elements.filter(child => child !== this.node && !(child.hasAttribute('inert')));
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
  });
@@ -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
+ });