Skip to content

Commit

Permalink
Track source locations when parsing and serializing
Browse files Browse the repository at this point in the history
  • Loading branch information
thecrypticace committed Apr 18, 2024
1 parent 5a01756 commit 3f36544
Show file tree
Hide file tree
Showing 2 changed files with 136 additions and 4 deletions.
5 changes: 5 additions & 0 deletions packages/tailwindcss/src/css-parser.bench.ts
Original file line number Diff line number Diff line change
@@ -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`
Expand Down Expand Up @@ -30,3 +31,7 @@ bench('CSS', () => {
trackSource: false,
})
})

bench('CSS with sourcemaps', () => {
CSS.parse(input, new TrackLocations())
})
135 changes: 131 additions & 4 deletions packages/tailwindcss/src/css-parser.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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[] = []
Expand All @@ -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.
//
Expand Down Expand Up @@ -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
Expand All @@ -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()!)
}
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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()!)
}
}
}
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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
}

Expand Down

0 comments on commit 3f36544

Please sign in to comment.