Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -583,3 +583,59 @@ export function format(
export function minify(css: string): string {
return format(css, { minify: true })
}

export const LINE_TYPE_SELECTOR = 1
export const LINE_TYPE_DECLARATION = 2
export const LINE_TYPE_BRACKET = 3
export const LINE_TYPE_ATRULE = 4
export const LINE_TYPE_COMMENT = 5
export const LINE_TYPE_EMPTY = 6

export type LineType =
| typeof LINE_TYPE_SELECTOR
| typeof LINE_TYPE_DECLARATION
| typeof LINE_TYPE_BRACKET
| typeof LINE_TYPE_ATRULE
| typeof LINE_TYPE_COMMENT
| typeof LINE_TYPE_EMPTY

function classify_lines(lines: string[]): LineType[] {
let types: LineType[] = []
let in_multiline_comment = false

for (let line of lines) {
let trimmed = line.trimEnd()

if (in_multiline_comment) {
types.push(LINE_TYPE_COMMENT)
if (trimmed.includes('*/')) in_multiline_comment = false
continue
}

if (trimmed === '') {
types.push(LINE_TYPE_EMPTY)
} else if (trimmed.trimStart().startsWith('/*')) {
types.push(LINE_TYPE_COMMENT)
if (!trimmed.slice(trimmed.indexOf('/*') + 2).includes('*/')) in_multiline_comment = true
} else if (trimmed.endsWith(CLOSE_BRACE)) {
types.push(LINE_TYPE_BRACKET)
} else if (trimmed.trimStart().startsWith('@')) {
types.push(LINE_TYPE_ATRULE)
} else if (trimmed.endsWith(OPEN_BRACE) || trimmed.endsWith(COMMA)) {
types.push(LINE_TYPE_SELECTOR)
} else {
types.push(LINE_TYPE_DECLARATION)
}
}

return types
}

export function format_with_types(
css: string,
options: Omit<FormatOptions, 'minify'> = Object.create(null),
): { css: string; types: LineType[] } {
let formatted = format(css, options)
let types = classify_lines(formatted.split('\n'))
return { css: formatted, types }
}
156 changes: 156 additions & 0 deletions test/format-with-types.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { describe, test, expect } from 'vitest'
import {
format_with_types,
LINE_TYPE_SELECTOR,
LINE_TYPE_DECLARATION,
LINE_TYPE_BRACKET,
LINE_TYPE_ATRULE,
LINE_TYPE_COMMENT,
LINE_TYPE_EMPTY,
} from '../src/lib/index.js'

describe('format_with_types', () => {
test('simple rule', () => {
let { css, types } = format_with_types('a { color: red; }')
expect(css).toBe('a {\n\tcolor: red;\n}')
expect(types).toEqual([LINE_TYPE_SELECTOR, LINE_TYPE_DECLARATION, LINE_TYPE_BRACKET])
})

test('multiple declarations', () => {
let { css, types } = format_with_types('a { color: red; background: blue; }')
expect(types).toEqual([
LINE_TYPE_SELECTOR,
LINE_TYPE_DECLARATION,
LINE_TYPE_DECLARATION,
LINE_TYPE_BRACKET,
])
})

test('multi-line selector list', () => {
let { css, types } = format_with_types('a, b { color: red; }')
expect(css).toBe('a,\nb {\n\tcolor: red;\n}')
expect(types).toEqual([
LINE_TYPE_SELECTOR,
LINE_TYPE_SELECTOR,
LINE_TYPE_DECLARATION,
LINE_TYPE_BRACKET,
])
})

test('atrule with block', () => {
let { css, types } = format_with_types('@media all { a { color: red } }')
expect(css).toBe('@media all {\n\ta {\n\t\tcolor: red;\n\t}\n}')
expect(types).toEqual([
LINE_TYPE_ATRULE,
LINE_TYPE_SELECTOR,
LINE_TYPE_DECLARATION,
LINE_TYPE_BRACKET,
LINE_TYPE_BRACKET,
])
})

test('atrule without block (@charset)', () => {
let { css, types } = format_with_types('@charset "utf-8";')
expect(css).toBe('@charset "utf-8";')
expect(types).toEqual([LINE_TYPE_ATRULE])
})

test('atrule without block (@import)', () => {
let { css, types } = format_with_types('@import "foo.css";')
expect(css).toBe('@import "foo.css";')
expect(types).toEqual([LINE_TYPE_ATRULE])
})

test('empty line between rules', () => {
let { css, types } = format_with_types('a {} b {}')
expect(css).toBe('a {}\n\nb {}')
expect(types).toEqual([LINE_TYPE_BRACKET, LINE_TYPE_EMPTY, LINE_TYPE_BRACKET])
})

test('single-line comment before rule', () => {
let { css, types } = format_with_types('/* comment */ a { color: red; }')
expect(css).toBe('/* comment */\na {\n\tcolor: red;\n}')
expect(types).toEqual([
LINE_TYPE_COMMENT,
LINE_TYPE_SELECTOR,
LINE_TYPE_DECLARATION,
LINE_TYPE_BRACKET,
])
})

test('single-line comment inside rule', () => {
let { css, types } = format_with_types('a { /* comment */ color: red; }')
expect(css).toBe('a {\n\t/* comment */\n\tcolor: red;\n}')
expect(types).toEqual([
LINE_TYPE_SELECTOR,
LINE_TYPE_COMMENT,
LINE_TYPE_DECLARATION,
LINE_TYPE_BRACKET,
])
})

test('multiline comment before rule', () => {
let { css, types } = format_with_types('/* line 1\n line 2 */ a { color: red; }')
expect(css).toBe('/* line 1\n line 2 */\na {\n\tcolor: red;\n}')
expect(types).toEqual([
LINE_TYPE_COMMENT,
LINE_TYPE_COMMENT,
LINE_TYPE_SELECTOR,
LINE_TYPE_DECLARATION,
LINE_TYPE_BRACKET,
])
})

test('multiline comment inside rule', () => {
let { css, types } = format_with_types(
'a {\n\t/* line 1\n\t line 2 */\n\tcolor: red;\n}',
)
expect(css).toBe('a {\n\t/* line 1\n\t line 2 */\n\tcolor: red;\n}')
expect(types).toEqual([
LINE_TYPE_SELECTOR,
LINE_TYPE_COMMENT,
LINE_TYPE_COMMENT,
LINE_TYPE_DECLARATION,
LINE_TYPE_BRACKET,
])
})

test('multiline comment spanning three lines', () => {
let { css, types } = format_with_types('/* a\n * b\n * c */ a { color: red; }')
expect(css).toBe('/* a\n * b\n * c */\na {\n\tcolor: red;\n}')
expect(types).toEqual([
LINE_TYPE_COMMENT,
LINE_TYPE_COMMENT,
LINE_TYPE_COMMENT,
LINE_TYPE_SELECTOR,
LINE_TYPE_DECLARATION,
LINE_TYPE_BRACKET,
])
})

test('comment containing a semicolon is not a declaration', () => {
let { css, types } = format_with_types('a { /* color: red; */ color: blue; }')
expect(types).toEqual([
LINE_TYPE_SELECTOR,
LINE_TYPE_COMMENT,
LINE_TYPE_DECLARATION,
LINE_TYPE_BRACKET,
])
})

test('comment containing a brace is not a bracket', () => {
let { css, types } = format_with_types('/* } */ a { color: red; }')
expect(types[0]).toBe(LINE_TYPE_COMMENT)
})

test('types array length matches number of lines', () => {
let input = '@media all { a, b { color: red; background: blue; } }'
let { css, types } = format_with_types(input)
expect(types).toHaveLength(css.split('\n').length)
})

test('tab_size option is forwarded', () => {
let { css } = format_with_types('a { color: red; }', { tab_size: 2 })
expect(css).toBe('a {\n color: red;\n}')
})
})
Loading