Skip to content

Commit a2b1815

Browse files
Forward css.format.* settings to js-beautify's CSS sub-formatter (#238)
The HTML formatter uses js-beautify which already supports CSS formatting options via a css sub-object, but HTMLFormatConfiguration never passed them through. This caused css.format.newlineBetweenSelectors, newlineBetweenRules, and other CSS settings to be ignored when formatting embedded CSS in <style> tags. - Add EmbeddedCSSFormatConfiguration interface to HTMLFormatConfiguration - Map camelCase CSS options to snake_case js-beautify options in htmlFormatter - Add css property to IBeautifyHTMLOptions type declaration - Add tests for embedded CSS formatting options Fixes microsoft/vscode#283316
1 parent bd99c28 commit a2b1815

4 files changed

Lines changed: 271 additions & 1 deletion

File tree

src/beautify/beautify-html.d.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,19 @@ export interface IBeautifyHTMLOptions {
123123
* default ""
124124
*/
125125
unformatted_content_delimiter?: string;
126+
127+
/**
128+
* Options for the CSS sub-formatter used when formatting embedded CSS in style tags.
129+
* Properties in this object are promoted to top-level CSS options via js-beautify's _mergeOpts.
130+
*/
131+
css?: {
132+
selector_separator_newline?: boolean;
133+
newline_between_rules?: boolean;
134+
space_around_selector_separator?: boolean;
135+
brace_style?: 'collapse' | 'expand';
136+
preserve_newlines?: boolean;
137+
max_preserve_newlines?: number;
138+
};
126139
}
127140

128141
export interface IBeautifyHTML {

src/htmlLanguageTypes.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,20 @@ export interface HTMLFormatConfiguration {
4747
templating?: ('auto' | 'none' | 'angular' | 'django' | 'erb' | 'handlebars' | 'php' | 'smarty')[] | boolean;
4848
unformattedContentDelimiter?: string;
4949

50+
/**
51+
* Options for formatting embedded CSS in style tags.
52+
* These are forwarded to the CSS sub-formatter used by js-beautify.
53+
*/
54+
css?: EmbeddedCSSFormatConfiguration;
55+
}
56+
57+
export interface EmbeddedCSSFormatConfiguration {
58+
newlineBetweenSelectors?: boolean;
59+
newlineBetweenRules?: boolean;
60+
spaceAroundSelectorSeparator?: boolean;
61+
braceStyle?: 'collapse' | 'expand';
62+
preserveNewLines?: boolean;
63+
maxPreserveNewLines?: number;
5064
}
5165

5266
export interface HoverSettings {

src/services/htmlFormatter.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ export function format(document: TextDocument, range: Range | undefined, options
8181
indent_scripts: getFormatOption(options, 'indentScripts', 'normal'),
8282
templating: getTemplatingFormatOption(options, 'all'),
8383
unformatted_content_delimiter: getFormatOption(options, 'unformattedContentDelimiter', ''),
84+
css: getCSSFormatOption(options),
8485
};
8586

8687
let result = html_beautify(trimLeft(value), htmlOptions);
@@ -133,6 +134,21 @@ function getTemplatingFormatOption(options: HTMLFormatConfiguration, dflt: strin
133134
return value;
134135
}
135136

137+
function getCSSFormatOption(options: HTMLFormatConfiguration): IBeautifyHTMLOptions['css'] {
138+
const css = options.css;
139+
if (!css) {
140+
return undefined;
141+
}
142+
return {
143+
selector_separator_newline: css.newlineBetweenSelectors,
144+
newline_between_rules: css.newlineBetweenRules,
145+
space_around_selector_separator: css.spaceAroundSelectorSeparator,
146+
brace_style: css.braceStyle,
147+
preserve_newlines: css.preserveNewLines,
148+
max_preserve_newlines: css.maxPreserveNewLines,
149+
};
150+
}
151+
136152
function computeIndentLevel(content: string, offset: number, options: HTMLFormatConfiguration): number {
137153
let i = offset;
138154
let nChars = 0;

src/test/formatter.test.ts

Lines changed: 228 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55
import { suite, test } from 'node:test';
6-
import { getLanguageService, TextDocument, Range } from '../htmlLanguageService.js';
6+
import { getLanguageService, TextDocument, Range, HTMLFormatConfiguration } from '../htmlLanguageService.js';
77
import * as assert from 'node:assert';
88

99
suite('HTML Formatter', () => {
@@ -347,3 +347,230 @@ suite('HTML Formatter', () => {
347347
});
348348

349349
});
350+
351+
suite('HTML Formatter - Embedded CSS', () => {
352+
353+
function formatWithOptions(unformatted: string, expected: string, options: HTMLFormatConfiguration) {
354+
const uri = 'test://test.html';
355+
const document = TextDocument.create(uri, 'html', 0, unformatted);
356+
const edits = getLanguageService().format(document, undefined, options);
357+
const formatted = TextDocument.applyEdits(document, edits);
358+
assert.equal(formatted, expected);
359+
}
360+
361+
test('css.newlineBetweenSelectors: false', () => {
362+
var content = [
363+
'<html>',
364+
'',
365+
'<head>',
366+
' <style>',
367+
' h1, h2 { color: red; }',
368+
' </style>',
369+
'</head>',
370+
'',
371+
'</html>',
372+
].join('\n');
373+
374+
var expected = [
375+
'<html>',
376+
'',
377+
'<head>',
378+
' <style>',
379+
' h1, h2 {',
380+
' color: red;',
381+
' }',
382+
' </style>',
383+
'</head>',
384+
'',
385+
'</html>',
386+
].join('\n');
387+
388+
formatWithOptions(content, expected, {
389+
tabSize: 2,
390+
insertSpaces: true,
391+
css: { newlineBetweenSelectors: false }
392+
});
393+
});
394+
395+
test('css.newlineBetweenSelectors: true (default)', () => {
396+
var content = [
397+
'<html>',
398+
'',
399+
'<head>',
400+
' <style>',
401+
' h1, h2 { color: red; }',
402+
' </style>',
403+
'</head>',
404+
'',
405+
'</html>',
406+
].join('\n');
407+
408+
var expected = [
409+
'<html>',
410+
'',
411+
'<head>',
412+
' <style>',
413+
' h1,',
414+
' h2 {',
415+
' color: red;',
416+
' }',
417+
' </style>',
418+
'</head>',
419+
'',
420+
'</html>',
421+
].join('\n');
422+
423+
formatWithOptions(content, expected, {
424+
tabSize: 2,
425+
insertSpaces: true,
426+
css: { newlineBetweenSelectors: true }
427+
});
428+
});
429+
430+
test('css.newlineBetweenRules: false', () => {
431+
var content = [
432+
'<html>',
433+
'',
434+
'<head>',
435+
' <style>',
436+
' h1 { color: red; }',
437+
' h2 { color: blue; }',
438+
' </style>',
439+
'</head>',
440+
'',
441+
'</html>',
442+
].join('\n');
443+
444+
var expected = [
445+
'<html>',
446+
'',
447+
'<head>',
448+
' <style>',
449+
' h1 {',
450+
' color: red;',
451+
' }',
452+
' h2 {',
453+
' color: blue;',
454+
' }',
455+
' </style>',
456+
'</head>',
457+
'',
458+
'</html>',
459+
].join('\n');
460+
461+
formatWithOptions(content, expected, {
462+
tabSize: 2,
463+
insertSpaces: true,
464+
css: { newlineBetweenRules: false }
465+
});
466+
});
467+
468+
test('css.newlineBetweenRules: true (default)', () => {
469+
var content = [
470+
'<html>',
471+
'',
472+
'<head>',
473+
' <style>',
474+
' h1 { color: red; }',
475+
' h2 { color: blue; }',
476+
' </style>',
477+
'</head>',
478+
'',
479+
'</html>',
480+
].join('\n');
481+
482+
var expected = [
483+
'<html>',
484+
'',
485+
'<head>',
486+
' <style>',
487+
' h1 {',
488+
' color: red;',
489+
' }',
490+
'',
491+
' h2 {',
492+
' color: blue;',
493+
' }',
494+
' </style>',
495+
'</head>',
496+
'',
497+
'</html>',
498+
].join('\n');
499+
500+
formatWithOptions(content, expected, {
501+
tabSize: 2,
502+
insertSpaces: true,
503+
css: { newlineBetweenRules: true }
504+
});
505+
});
506+
507+
test('css.spaceAroundSelectorSeparator: true', () => {
508+
var content = [
509+
'<html>',
510+
'',
511+
'<head>',
512+
' <style>',
513+
' div>span { color: red; }',
514+
' </style>',
515+
'</head>',
516+
'',
517+
'</html>',
518+
].join('\n');
519+
520+
var expected = [
521+
'<html>',
522+
'',
523+
'<head>',
524+
' <style>',
525+
' div > span {',
526+
' color: red;',
527+
' }',
528+
' </style>',
529+
'</head>',
530+
'',
531+
'</html>',
532+
].join('\n');
533+
534+
formatWithOptions(content, expected, {
535+
tabSize: 2,
536+
insertSpaces: true,
537+
css: { spaceAroundSelectorSeparator: true }
538+
});
539+
});
540+
541+
test('no css options passed - uses defaults', () => {
542+
var content = [
543+
'<html>',
544+
'',
545+
'<head>',
546+
' <style>',
547+
' h1, h2 { color: red; }',
548+
' </style>',
549+
'</head>',
550+
'',
551+
'</html>',
552+
].join('\n');
553+
554+
// Default: newlineBetweenSelectors is true
555+
var expected = [
556+
'<html>',
557+
'',
558+
'<head>',
559+
' <style>',
560+
' h1,',
561+
' h2 {',
562+
' color: red;',
563+
' }',
564+
' </style>',
565+
'</head>',
566+
'',
567+
'</html>',
568+
].join('\n');
569+
570+
formatWithOptions(content, expected, {
571+
tabSize: 2,
572+
insertSpaces: true,
573+
});
574+
});
575+
576+
});

0 commit comments

Comments
 (0)