diff --git a/src/beautify/beautify-html.d.ts b/src/beautify/beautify-html.d.ts index 59f4940..66ca4a6 100644 --- a/src/beautify/beautify-html.d.ts +++ b/src/beautify/beautify-html.d.ts @@ -123,6 +123,19 @@ export interface IBeautifyHTMLOptions { * default "" */ unformatted_content_delimiter?: string; + + /** + * Options for the CSS sub-formatter used when formatting embedded CSS in style tags. + * Properties in this object are promoted to top-level CSS options via js-beautify's _mergeOpts. + */ + css?: { + selector_separator_newline?: boolean; + newline_between_rules?: boolean; + space_around_selector_separator?: boolean; + brace_style?: 'collapse' | 'expand'; + preserve_newlines?: boolean; + max_preserve_newlines?: number; + }; } export interface IBeautifyHTML { diff --git a/src/htmlLanguageTypes.ts b/src/htmlLanguageTypes.ts index 83d4b08..fd75c7e 100644 --- a/src/htmlLanguageTypes.ts +++ b/src/htmlLanguageTypes.ts @@ -47,6 +47,20 @@ export interface HTMLFormatConfiguration { templating?: ('auto' | 'none' | 'angular' | 'django' | 'erb' | 'handlebars' | 'php' | 'smarty')[] | boolean; unformattedContentDelimiter?: string; + /** + * Options for formatting embedded CSS in style tags. + * These are forwarded to the CSS sub-formatter used by js-beautify. + */ + css?: EmbeddedCSSFormatConfiguration; +} + +export interface EmbeddedCSSFormatConfiguration { + newlineBetweenSelectors?: boolean; + newlineBetweenRules?: boolean; + spaceAroundSelectorSeparator?: boolean; + braceStyle?: 'collapse' | 'expand'; + preserveNewLines?: boolean; + maxPreserveNewLines?: number; } export interface HoverSettings { diff --git a/src/services/htmlFormatter.ts b/src/services/htmlFormatter.ts index e0f43c3..b9ec4d7 100644 --- a/src/services/htmlFormatter.ts +++ b/src/services/htmlFormatter.ts @@ -81,6 +81,7 @@ export function format(document: TextDocument, range: Range | undefined, options indent_scripts: getFormatOption(options, 'indentScripts', 'normal'), templating: getTemplatingFormatOption(options, 'all'), unformatted_content_delimiter: getFormatOption(options, 'unformattedContentDelimiter', ''), + css: getCSSFormatOption(options), }; let result = html_beautify(trimLeft(value), htmlOptions); @@ -133,6 +134,21 @@ function getTemplatingFormatOption(options: HTMLFormatConfiguration, dflt: strin return value; } +function getCSSFormatOption(options: HTMLFormatConfiguration): IBeautifyHTMLOptions['css'] { + const css = options.css; + if (!css) { + return undefined; + } + return { + selector_separator_newline: css.newlineBetweenSelectors, + newline_between_rules: css.newlineBetweenRules, + space_around_selector_separator: css.spaceAroundSelectorSeparator, + brace_style: css.braceStyle, + preserve_newlines: css.preserveNewLines, + max_preserve_newlines: css.maxPreserveNewLines, + }; +} + function computeIndentLevel(content: string, offset: number, options: HTMLFormatConfiguration): number { let i = offset; let nChars = 0; diff --git a/src/test/formatter.test.ts b/src/test/formatter.test.ts index d67fb0b..0e50a7f 100644 --- a/src/test/formatter.test.ts +++ b/src/test/formatter.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { suite, test } from 'node:test'; -import { getLanguageService, TextDocument, Range } from '../htmlLanguageService.js'; +import { getLanguageService, TextDocument, Range, HTMLFormatConfiguration } from '../htmlLanguageService.js'; import * as assert from 'node:assert'; suite('HTML Formatter', () => { @@ -347,3 +347,230 @@ suite('HTML Formatter', () => { }); }); + +suite('HTML Formatter - Embedded CSS', () => { + + function formatWithOptions(unformatted: string, expected: string, options: HTMLFormatConfiguration) { + const uri = 'test://test.html'; + const document = TextDocument.create(uri, 'html', 0, unformatted); + const edits = getLanguageService().format(document, undefined, options); + const formatted = TextDocument.applyEdits(document, edits); + assert.equal(formatted, expected); + } + + test('css.newlineBetweenSelectors: false', () => { + var content = [ + '', + '', + '
', + ' ', + '', + '', + '', + ].join('\n'); + + var expected = [ + '', + '', + '', + ' ', + '', + '', + '', + ].join('\n'); + + formatWithOptions(content, expected, { + tabSize: 2, + insertSpaces: true, + css: { newlineBetweenSelectors: false } + }); + }); + + test('css.newlineBetweenSelectors: true (default)', () => { + var content = [ + '', + '', + '', + ' ', + '', + '', + '', + ].join('\n'); + + var expected = [ + '', + '', + '', + ' ', + '', + '', + '', + ].join('\n'); + + formatWithOptions(content, expected, { + tabSize: 2, + insertSpaces: true, + css: { newlineBetweenSelectors: true } + }); + }); + + test('css.newlineBetweenRules: false', () => { + var content = [ + '', + '', + '', + ' ', + '', + '', + '', + ].join('\n'); + + var expected = [ + '', + '', + '', + ' ', + '', + '', + '', + ].join('\n'); + + formatWithOptions(content, expected, { + tabSize: 2, + insertSpaces: true, + css: { newlineBetweenRules: false } + }); + }); + + test('css.newlineBetweenRules: true (default)', () => { + var content = [ + '', + '', + '', + ' ', + '', + '', + '', + ].join('\n'); + + var expected = [ + '', + '', + '', + ' ', + '', + '', + '', + ].join('\n'); + + formatWithOptions(content, expected, { + tabSize: 2, + insertSpaces: true, + css: { newlineBetweenRules: true } + }); + }); + + test('css.spaceAroundSelectorSeparator: true', () => { + var content = [ + '', + '', + '', + ' ', + '', + '', + '', + ].join('\n'); + + var expected = [ + '', + '', + '', + ' ', + '', + '', + '', + ].join('\n'); + + formatWithOptions(content, expected, { + tabSize: 2, + insertSpaces: true, + css: { spaceAroundSelectorSeparator: true } + }); + }); + + test('no css options passed - uses defaults', () => { + var content = [ + '', + '', + '', + ' ', + '', + '', + '', + ].join('\n'); + + // Default: newlineBetweenSelectors is true + var expected = [ + '', + '', + '', + ' ', + '', + '', + '', + ].join('\n'); + + formatWithOptions(content, expected, { + tabSize: 2, + insertSpaces: true, + }); + }); + +});