From 3f3654439661230f2f027a47be6948bbb5110de3 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 17 Apr 2024 17:05:31 -0400 Subject: [PATCH] Track source locations when parsing and serializing --- packages/tailwindcss/src/css-parser.bench.ts | 5 + packages/tailwindcss/src/css-parser.ts | 135 ++++++++++++++++++- 2 files changed, 136 insertions(+), 4 deletions(-) diff --git a/packages/tailwindcss/src/css-parser.bench.ts b/packages/tailwindcss/src/css-parser.bench.ts index b9c2cc83973f..42051e786523 100644 --- a/packages/tailwindcss/src/css-parser.bench.ts +++ b/packages/tailwindcss/src/css-parser.bench.ts @@ -1,5 +1,6 @@ import { bench } from 'vitest' import * as CSS from './css-parser' +import { TrackLocations } from './track-locations' const css = String.raw const input = css` @@ -30,3 +31,7 @@ bench('CSS', () => { trackSource: false, }) }) + +bench('CSS with sourcemaps', () => { + CSS.parse(input, new TrackLocations()) +}) diff --git a/packages/tailwindcss/src/css-parser.ts b/packages/tailwindcss/src/css-parser.ts index 88a2a0640e4c..3cd40de44319 100644 --- a/packages/tailwindcss/src/css-parser.ts +++ b/packages/tailwindcss/src/css-parser.ts @@ -1,4 +1,5 @@ import { comment, rule, type AstNode, type Comment, type Declaration, type Rule } from './ast' +import type { TrackLocations } from './track-locations' const BACKSLASH = 0x5c const SLASH = 0x2f @@ -20,7 +21,7 @@ const DASH = 0x2d const AT_SIGN = 0x40 const EXCLAMATION_MARK = 0x21 -export function parse(input: string) { +export function parse(input: string, track?: TrackLocations) { input = input.replaceAll('\r\n', '\n') let ast: AstNode[] = [] @@ -36,9 +37,48 @@ export function parse(input: string) { let peekChar + // The current line number + let line = 1 + + // The index of the first non-whitespace character on the current line + let lineStart = 0 + + // Source location tracking + let sourceStartLine = 1 + let sourceStartColumn = 0 + let sourceEndLine = 1 + let sourceEndColumn = 0 + + function sourceRange() { + if (!track) return null + + return { + start: { + line: sourceStartLine, + column: sourceStartColumn, + }, + end: { + line: sourceEndLine, + column: sourceEndColumn, + }, + } + } + for (let i = 0; i < input.length; i++) { let currentChar = input.charCodeAt(i) + if (currentChar === LINE_BREAK) { + line += 1 + lineStart = i + 1 + + if (buffer.length === 0) { + sourceStartLine = line + sourceStartColumn = 0 + sourceEndLine = line + sourceEndColumn = 0 + } + } + // Current character is a `\` therefore the next character is escaped, // consume it together with the next character and continue. // @@ -81,6 +121,19 @@ export function parse(input: string) { j += 1 } + // Count newline within comments + else if (peekChar === LINE_BREAK) { + line += 1 + lineStart = j + 1 + + if (buffer.length === 0) { + sourceStartLine = line + sourceStartColumn = 0 + sourceEndLine = line + sourceEndColumn = 0 + } + } + // End of the comment else if (peekChar === ASTERISK && input.charCodeAt(j + 1) === SLASH) { i = j + 1 @@ -93,7 +146,9 @@ export function parse(input: string) { // Collect all license comments so that we can hoist them to the top of // the AST. if (commentString.charCodeAt(2) === EXCLAMATION_MARK) { - licenseComments.push(comment(commentString.slice(2, -2))) + let node = comment(commentString.slice(2, -2)) + licenseComments.push(node) + track?.src(node, sourceRange()!) } } @@ -211,6 +266,19 @@ export function parse(input: string) { k += 1 } + // Count newline within comments + else if (peekChar === LINE_BREAK) { + line += 1 + lineStart = j + 1 + + if (buffer.length === 0) { + sourceStartLine = line + sourceStartColumn = 0 + sourceEndLine = line + sourceEndColumn = 0 + } + } + // End of the comment else if (peekChar === ASTERISK && input.charCodeAt(k + 1) === SLASH) { j = k + 1 @@ -273,6 +341,19 @@ export function parse(input: string) { closingBracketStack = closingBracketStack.slice(0, -1) } } + + // Count newlines + else if (peekChar === LINE_BREAK) { + line += 1 + lineStart = j + 1 + + if (buffer.length === 0) { + sourceStartLine = line + sourceStartColumn = 0 + sourceEndLine = line + sourceEndColumn = 0 + } + } } let declaration = parseDeclaration(buffer, colonIdx) @@ -281,8 +362,13 @@ export function parse(input: string) { } else { ast.push(declaration) } + track?.src(declaration, sourceRange()!) buffer = '' + sourceStartLine = line + sourceStartColumn = i - lineStart + sourceEndLine = line + sourceEndColumn = i - lineStart } // End of a body-less at-rule. @@ -306,9 +392,16 @@ export function parse(input: string) { ast.push(node) } + // Track the source location for source maps + track?.src(node, sourceRange()!) + // Reset the state for the next node. buffer = '' node = null + sourceStartLine = line + sourceStartColumn = i - lineStart + sourceEndLine = line + sourceEndColumn = i - lineStart } // End of a declaration. @@ -330,7 +423,14 @@ export function parse(input: string) { ast.push(declaration) } + // Track the source location for source maps + track?.src(declaration, sourceRange()!) + buffer = '' + sourceStartLine = line + sourceStartColumn = i - lineStart + sourceEndLine = line + sourceEndColumn = i - lineStart } // Start of a block. @@ -353,9 +453,16 @@ export function parse(input: string) { // attached to it. parent = node + // Track the source location for source maps + track?.src(node, sourceRange()!) + // Reset the state for the next node. buffer = '' node = null + sourceStartLine = line + sourceStartColumn = i - lineStart + sourceEndLine = line + sourceEndColumn = i - lineStart } // End of a block. @@ -393,9 +500,16 @@ export function parse(input: string) { ast.push(node) } + // Track the source location for source maps + track?.src(node, sourceRange()!) + // Reset the state for the next node. buffer = '' node = null + sourceStartLine = line + sourceStartColumn = i - lineStart + sourceEndLine = line + sourceEndColumn = i - lineStart } // But it can also happen for declarations. @@ -417,14 +531,16 @@ export function parse(input: string) { // Attach the declaration to the parent. if (parent) { let importantIdx = buffer.indexOf('!important', colonIdx + 1) - parent.nodes.push({ + let node = { kind: 'declaration', property: buffer.slice(0, colonIdx).trim(), value: buffer .slice(colonIdx + 1, importantIdx === -1 ? buffer.length : importantIdx) .trim(), important: importantIdx !== -1, - } satisfies Declaration) + } satisfies Declaration + parent.nodes.push(node) + track?.src(node, sourceRange()!) } } } @@ -437,6 +553,9 @@ export function parse(input: string) { // node. if (grandParent === null && parent) { ast.push(parent) + + // We want to track the closing `}` as part of the parent node. + track?.src(parent, sourceRange()!) } // Go up one level in the stack. @@ -445,6 +564,10 @@ export function parse(input: string) { // Reset the state for the next node. buffer = '' node = null + sourceStartLine = line + sourceStartColumn = i - lineStart + sourceEndLine = line + sourceEndColumn = i - lineStart } // Any other character is part of the current node. @@ -454,6 +577,10 @@ export function parse(input: string) { buffer.length === 0 && (currentChar === SPACE || currentChar === LINE_BREAK || currentChar === TAB) ) { + sourceStartLine = line + sourceStartColumn = i + 1 - lineStart + sourceEndLine = line + sourceEndColumn = i + 1 - lineStart continue }