From 8ea04e472842ed4b904662c3f91f8885dcd3960d Mon Sep 17 00:00:00 2001 From: Nick Trogh Date: Wed, 18 Mar 2026 15:52:42 +0100 Subject: [PATCH] Implement conditional blocks processing in release notes (#302811) * feat: implement conditional blocks processing in release notes * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Fix compilation issue --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../update/browser/releaseNotesEditor.ts | 46 ++++++- .../test/browser/releaseNotesRenderer.test.ts | 128 +++++++++++++++++- 2 files changed, 172 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts b/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts index 2d41bd125be..2cd16781ec5 100644 --- a/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts +++ b/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts @@ -275,7 +275,7 @@ export class ReleaseNotesManager extends Disposable { private async renderBody(fileContent: { text: string; base: URI }) { const nonce = generateUuid(); - const processedContent = await renderReleaseNotesMarkdown(fileContent.text, this._extensionService, this._languageService, this._simpleSettingRenderer); + const processedContent = await renderReleaseNotesMarkdown(fileContent.text, this._extensionService, this._languageService, this._simpleSettingRenderer, this._productService.quality); const colorMap = TokenizationRegistry.getColorMap(); const css = colorMap ? generateTokensCSSForColorMap(colorMap) : ''; @@ -774,11 +774,46 @@ export class ReleaseNotesManager extends Disposable { } } +/** + * Processes conditional blocks in the release notes markdown. + * + * Conditional blocks use a single HTML comment with the format: + * ``` + * + * ``` + * + * Supported conditions: + * - `IN_PRODUCT` - Content shown in VS Code (both Stable and Insiders) + * - `WEB` - Content shown on the website only + * - `STABLE` - Content shown in VS Code Stable only + * - `INSIDERS` - Content shown in VS Code Insiders only + * + * On the website, the entire block is a single HTML comment, so the + * content is hidden by default. The website renderer would activate + * `WEB` blocks by stripping the comment markers. + */ +export function processConditionalBlocks(text: string, activeConditions: ReadonlySet): string { + return text.replace( + //gi, + (_match, condition: string, content: string) => { + if (activeConditions.has(condition.toUpperCase())) { + // Strip comment markers, reveal content + return content; + } + // Remove the entire block + return ''; + } + ); +} + export async function renderReleaseNotesMarkdown( text: string, extensionService: IExtensionService, languageService: ILanguageService, simpleSettingRenderer: SimpleSettingRenderer, + quality?: string, ): Promise { // Remove HTML comment markers around table of contents navigation text = text @@ -786,6 +821,15 @@ export async function renderReleaseNotesMarkdown( .replace(//gi, ''); + // Process conditional blocks based on active conditions + const activeConditions = new Set(['IN_PRODUCT']); + if (quality === 'stable') { + activeConditions.add('STABLE'); + } else if (quality === 'insider') { + activeConditions.add('INSIDERS'); + } + text = processConditionalBlocks(text, activeConditions); + return renderMarkdownDocument(text, extensionService, languageService, { sanitizerConfig: { allowRelativeMediaPaths: true, diff --git a/src/vs/workbench/contrib/update/test/browser/releaseNotesRenderer.test.ts b/src/vs/workbench/contrib/update/test/browser/releaseNotesRenderer.test.ts index dddde3072ee..6e6f8b33589 100644 --- a/src/vs/workbench/contrib/update/test/browser/releaseNotesRenderer.test.ts +++ b/src/vs/workbench/contrib/update/test/browser/releaseNotesRenderer.test.ts @@ -2,6 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import assert from 'assert'; import { assertSnapshot } from '../../../../../base/test/common/snapshot.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { ILanguageService } from '../../../../../editor/common/languages/language.js'; @@ -11,7 +12,7 @@ import { TestInstantiationService } from '../../../../../platform/instantiation/ import { IExtensionService } from '../../../../services/extensions/common/extensions.js'; import { SimpleSettingRenderer } from '../../../markdown/browser/markdownSettingRenderer.js'; import { IPreferencesService } from '../../../../services/preferences/common/preferences.js'; -import { renderReleaseNotesMarkdown } from '../../browser/releaseNotesEditor.js'; +import { processConditionalBlocks, renderReleaseNotesMarkdown } from '../../browser/releaseNotesEditor.js'; import { URI } from '../../../../../base/common/uri.js'; import { Emitter } from '../../../../../base/common/event.js'; @@ -103,3 +104,128 @@ Navigation End --> await assertSnapshot(result.toString()); }); }); + +suite('Conditional blocks', () => { + + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + test('IN_PRODUCT block is revealed when IN_PRODUCT is active', () => { + const text = 'before\n\nafter'; + const result = processConditionalBlocks(text, new Set(['IN_PRODUCT'])); + assert.ok(result.includes('in-product content')); + assert.ok(!result.includes('%IF')); + assert.ok(result.includes('before')); + assert.ok(result.includes('after')); + }); + + test('WEB block is removed when only IN_PRODUCT is active', () => { + const text = 'before\n\nafter'; + const result = processConditionalBlocks(text, new Set(['IN_PRODUCT'])); + assert.ok(!result.includes('web-only content')); + assert.ok(result.includes('before')); + assert.ok(result.includes('after')); + }); + + test('STABLE block is revealed when STABLE is active', () => { + const text = 'before\n\nafter'; + const result = processConditionalBlocks(text, new Set(['IN_PRODUCT', 'STABLE'])); + assert.ok(result.includes('stable content')); + assert.ok(!result.includes('%IF')); + }); + + test('STABLE block is removed when INSIDERS is active', () => { + const text = 'before\n\nafter'; + const result = processConditionalBlocks(text, new Set(['IN_PRODUCT', 'INSIDERS'])); + assert.ok(!result.includes('stable content')); + assert.ok(result.includes('before')); + assert.ok(result.includes('after')); + }); + + test('INSIDERS block is revealed when INSIDERS is active', () => { + const text = 'before\n\nafter'; + const result = processConditionalBlocks(text, new Set(['IN_PRODUCT', 'INSIDERS'])); + assert.ok(result.includes('insiders content')); + assert.ok(!result.includes('%IF')); + }); + + test('INSIDERS block is removed when STABLE is active', () => { + const text = 'before\n\nafter'; + const result = processConditionalBlocks(text, new Set(['IN_PRODUCT', 'STABLE'])); + assert.ok(!result.includes('insiders content')); + }); + + test('Conditions are case-insensitive', () => { + const text = ''; + const result = processConditionalBlocks(text, new Set(['IN_PRODUCT'])); + assert.ok(result.includes('content')); + assert.ok(!result.includes('%IF')); + }); + + test('Multiple conditional blocks in same document', () => { + const text = [ + 'shared content', + '', + '', + '', + '', + 'more shared content', + ].join('\n'); + const result = processConditionalBlocks(text, new Set(['IN_PRODUCT', 'STABLE'])); + assert.ok(result.includes('shared content')); + assert.ok(result.includes('in-product only')); + assert.ok(!result.includes('web only')); + assert.ok(result.includes('stable only')); + assert.ok(!result.includes('insiders only')); + assert.ok(result.includes('more shared content')); + }); + + test('renderReleaseNotesMarkdown passes stable quality correctly', async function () { + const instantiationService = store.add(new TestInstantiationService()); + const extensionService = instantiationService.get(IExtensionService); + const languageService = instantiationService.get(ILanguageService); + instantiationService.stub(IContextMenuService, store.add(instantiationService.createInstance(ContextMenuService))); + + const content = [ + '## Title', + '', + '', + ].join('\n'); + const result = await renderReleaseNotesMarkdown(content, extensionService, languageService, instantiationService.createInstance(SimpleSettingRenderer), 'stable'); + const html = result.toString(); + assert.ok(html.includes('stable content')); + assert.ok(!html.includes('insiders content')); + }); + + test('renderReleaseNotesMarkdown passes insider quality correctly', async function () { + const instantiationService = store.add(new TestInstantiationService()); + const extensionService = instantiationService.get(IExtensionService); + const languageService = instantiationService.get(ILanguageService); + instantiationService.stub(IContextMenuService, store.add(instantiationService.createInstance(ContextMenuService))); + + const content = [ + '## Title', + '', + '', + ].join('\n'); + const result = await renderReleaseNotesMarkdown(content, extensionService, languageService, instantiationService.createInstance(SimpleSettingRenderer), 'insider'); + const html = result.toString(); + assert.ok(!html.includes('stable content')); + assert.ok(html.includes('insiders content')); + }); +});