99 type Declaration ,
1010 type Rule ,
1111} from './ast'
12- import type { Source } from './source-maps/source'
12+ import { createLineTable } from './source-maps/line-table'
13+ import type { Source , SourceLocation } from './source-maps/source'
1314
1415const BACKSLASH = 0x5c
1516const SLASH = 0x2f
@@ -36,6 +37,30 @@ export interface ParseOptions {
3637 from ?: string
3738}
3839
40+ /**
41+ * CSS syntax error with source location information.
42+ */
43+ export class CssSyntaxError extends Error {
44+ loc : SourceLocation | null
45+
46+ constructor ( message : string , loc : SourceLocation | null ) {
47+ if ( loc ) {
48+ let source = loc [ 0 ]
49+ let start = createLineTable ( source . code ) . find ( loc [ 1 ] )
50+ message = `${ source . file } :${ start . line } :${ start . column + 1 } : ${ message } `
51+ }
52+
53+ super ( message )
54+
55+ this . name = 'CssSyntaxError'
56+ this . loc = loc
57+
58+ if ( Error . captureStackTrace ) {
59+ Error . captureStackTrace ( this , CssSyntaxError )
60+ }
61+ }
62+ }
63+
3964export function parse ( input : string , opts ?: ParseOptions ) {
4065 let source : Source | null = opts ?. from ? { file : opts . from , code : input } : null
4166
@@ -138,7 +163,7 @@ export function parse(input: string, opts?: ParseOptions) {
138163
139164 // Start of a string.
140165 else if ( currentChar === SINGLE_QUOTE || currentChar === DOUBLE_QUOTE ) {
141- let end = parseString ( input , i , currentChar )
166+ let end = parseString ( input , i , currentChar , source )
142167
143168 // Adjust `buffer` to include the string.
144169 buffer += input . slice ( i , end + 1 )
@@ -192,7 +217,7 @@ export function parse(input: string, opts?: ParseOptions) {
192217
193218 // Start of a string.
194219 else if ( peekChar === SINGLE_QUOTE || peekChar === DOUBLE_QUOTE ) {
195- j = parseString ( input , j , peekChar )
220+ j = parseString ( input , j , peekChar , source )
196221 }
197222
198223 // Start of a comment.
@@ -269,7 +294,12 @@ export function parse(input: string, opts?: ParseOptions) {
269294 }
270295
271296 let declaration = parseDeclaration ( buffer , colonIdx )
272- if ( ! declaration ) throw new Error ( `Invalid custom property, expected a value` )
297+ if ( ! declaration ) {
298+ throw new CssSyntaxError (
299+ `Invalid custom property, expected a value` ,
300+ source ? [ source , start , i ] : null ,
301+ )
302+ }
273303
274304 if ( source ) {
275305 declaration . src = [ source , start , i ]
@@ -334,7 +364,10 @@ export function parse(input: string, opts?: ParseOptions) {
334364 let declaration = parseDeclaration ( buffer )
335365 if ( ! declaration ) {
336366 if ( buffer . length === 0 ) continue
337- throw new Error ( `Invalid declaration: \`${ buffer . trim ( ) } \`` )
367+ throw new CssSyntaxError (
368+ `Invalid declaration: \`${ buffer . trim ( ) } \`` ,
369+ source ? [ source , bufferStart , i ] : null ,
370+ )
338371 }
339372
340373 if ( source ) {
@@ -391,7 +424,7 @@ export function parse(input: string, opts?: ParseOptions) {
391424 closingBracketStack [ closingBracketStack . length - 1 ] !== ')'
392425 ) {
393426 if ( closingBracketStack === '' ) {
394- throw new Error ( 'Missing opening {' )
427+ throw new CssSyntaxError ( 'Missing opening {' , source ? [ source , i , i ] : null )
395428 }
396429
397430 closingBracketStack = closingBracketStack . slice ( 0 , - 1 )
@@ -453,7 +486,12 @@ export function parse(input: string, opts?: ParseOptions) {
453486 // Attach the declaration to the parent.
454487 if ( parent ) {
455488 let node = parseDeclaration ( buffer , colonIdx )
456- if ( ! node ) throw new Error ( `Invalid declaration: \`${ buffer . trim ( ) } \`` )
489+ if ( ! node ) {
490+ throw new CssSyntaxError (
491+ `Invalid declaration: \`${ buffer . trim ( ) } \`` ,
492+ source ? [ source , bufferStart , i ] : null ,
493+ )
494+ }
457495
458496 if ( source ) {
459497 node . src = [ source , bufferStart , i ]
@@ -492,7 +530,7 @@ export function parse(input: string, opts?: ParseOptions) {
492530 // `)`
493531 else if ( currentChar === CLOSE_PAREN ) {
494532 if ( closingBracketStack [ closingBracketStack . length - 1 ] !== ')' ) {
495- throw new Error ( 'Missing opening (' )
533+ throw new CssSyntaxError ( 'Missing opening (' , source ? [ source , i , i ] : null )
496534 }
497535
498536 closingBracketStack = closingBracketStack . slice ( 0 , - 1 )
@@ -534,10 +572,17 @@ export function parse(input: string, opts?: ParseOptions) {
534572 // have a leftover `parent`, then it means that we have an unterminated block.
535573 if ( closingBracketStack . length > 0 && parent ) {
536574 if ( parent . kind === 'rule' ) {
537- throw new Error ( `Missing closing } at ${ parent . selector } ` )
575+ throw new CssSyntaxError (
576+ `Missing closing } at ${ parent . selector } ` ,
577+ parent . src ? [ parent . src [ 0 ] , parent . src [ 1 ] , parent . src [ 1 ] ] : null ,
578+ )
538579 }
580+
539581 if ( parent . kind === 'at-rule' ) {
540- throw new Error ( `Missing closing } at ${ parent . name } ${ parent . params } ` )
582+ throw new CssSyntaxError (
583+ `Missing closing } at ${ parent . name } ${ parent . params } ` ,
584+ parent . src ? [ parent . src [ 0 ] , parent . src [ 1 ] , parent . src [ 1 ] ] : null ,
585+ )
541586 }
542587 }
543588
@@ -594,7 +639,12 @@ function parseDeclaration(
594639 )
595640}
596641
597- function parseString ( input : string , startIdx : number , quoteChar : number ) : number {
642+ function parseString (
643+ input : string ,
644+ startIdx : number ,
645+ quoteChar : number ,
646+ source : Source | null = null ,
647+ ) : number {
598648 let peekChar : number
599649
600650 // We need to ensure that the closing quote is the same as the opening
@@ -636,8 +686,9 @@ function parseString(input: string, startIdx: number, quoteChar: number): number
636686 ( input . charCodeAt ( i + 1 ) === LINE_BREAK ||
637687 ( input . charCodeAt ( i + 1 ) === CARRIAGE_RETURN && input . charCodeAt ( i + 2 ) === LINE_BREAK ) )
638688 ) {
639- throw new Error (
689+ throw new CssSyntaxError (
640690 `Unterminated string: ${ input . slice ( startIdx , i + 1 ) + String . fromCharCode ( quoteChar ) } ` ,
691+ source ? [ source , startIdx , i + 1 ] : null ,
641692 )
642693 }
643694
@@ -655,8 +706,9 @@ function parseString(input: string, startIdx: number, quoteChar: number): number
655706 peekChar === LINE_BREAK ||
656707 ( peekChar === CARRIAGE_RETURN && input . charCodeAt ( i + 1 ) === LINE_BREAK )
657708 ) {
658- throw new Error (
709+ throw new CssSyntaxError (
659710 `Unterminated string: ${ input . slice ( startIdx , i ) + String . fromCharCode ( quoteChar ) } ` ,
711+ source ? [ source , startIdx , i + 1 ] : null ,
660712 )
661713 }
662714 }
0 commit comments