Track locations when parsing

This commit is contained in:
Jordan Pittman 2025-04-16 17:36:57 -04:00
parent f08f89d7ba
commit e95e6e239f
4 changed files with 67 additions and 2 deletions

View file

@ -21,6 +21,7 @@ export type Resolver = (id: string, base: string) => Promise<string | false | un
export interface CompileOptions {
base: string
from?: string
onDependency: (path: string) => void
shouldRewriteUrls?: boolean
polyfills?: Polyfills
@ -31,6 +32,7 @@ export interface CompileOptions {
function createCompileOptions({
base,
from,
polyfills,
onDependency,
shouldRewriteUrls,
@ -41,6 +43,7 @@ function createCompileOptions({
return {
base,
polyfills,
from,
async loadModule(id: string, base: string) {
return loadModule(id, base, onDependency, customJsResolver)
},

View file

@ -10,3 +10,7 @@ const cssFile = readFileSync(currentFolder + './preflight.css', 'utf-8')
bench('css-parser on preflight.css', () => {
CSS.parse(cssFile)
})
bench('CSS with sourcemaps', () => {
CSS.parse(cssFile, { from: 'input.css' })
})

View file

@ -9,6 +9,7 @@ import {
type Declaration,
type Rule,
} from './ast'
import type { Source } from './source-maps/source'
const BACKSLASH = 0x5c
const SLASH = 0x2f
@ -31,7 +32,13 @@ const DASH = 0x2d
const AT_SIGN = 0x40
const EXCLAMATION_MARK = 0x21
export function parse(input: string) {
export interface ParseOptions {
from?: string
}
export function parse(input: string, opts?: ParseOptions) {
let source: Source | null = opts?.from ? { file: opts.from, code: input } : null
// Note: it is important that any transformations of the input string
// *before* processing do NOT change the length of the string. This
// would invalidate the mechanism used to track source locations.
@ -48,6 +55,9 @@ export function parse(input: string) {
let buffer = ''
let closingBracketStack = ''
// The start of the first non-whitespace character in the buffer
let bufferStart = 0
let peekChar
for (let i = 0; i < input.length; i++) {
@ -72,6 +82,7 @@ export function parse(input: string) {
// ```
//
if (currentChar === BACKSLASH) {
if (buffer === '') bufferStart = i
buffer += input.slice(i, i + 2)
i += 1
}
@ -117,6 +128,11 @@ export function parse(input: string) {
if (commentString.charCodeAt(2) === EXCLAMATION_MARK) {
let node = comment(commentString.slice(2, -2))
licenseComments.push(node)
if (source) {
node.src = [source, start, i + 1]
node.dst = [source, start, i + 1]
}
}
}
@ -313,6 +329,11 @@ export function parse(input: string) {
let declaration = parseDeclaration(buffer, colonIdx)
if (!declaration) throw new Error(`Invalid custom property, expected a value`)
if (source) {
declaration.src = [source, start, i]
declaration.dst = [source, start, i]
}
if (parent) {
parent.nodes.push(declaration)
} else {
@ -333,6 +354,11 @@ export function parse(input: string) {
else if (currentChar === SEMICOLON && buffer.charCodeAt(0) === AT_SIGN) {
node = parseAtRule(buffer)
if (source) {
node.src = [source, bufferStart, i]
node.dst = [source, bufferStart, i]
}
// At-rule is nested inside of a rule, attach it to the parent.
if (parent) {
parent.nodes.push(node)
@ -369,6 +395,11 @@ export function parse(input: string) {
throw new Error(`Invalid declaration: \`${buffer.trim()}\``)
}
if (source) {
declaration.src = [source, bufferStart, i]
declaration.dst = [source, bufferStart, i]
}
if (parent) {
parent.nodes.push(declaration)
} else {
@ -388,6 +419,12 @@ export function parse(input: string) {
// At this point `buffer` should resemble a selector or an at-rule.
node = rule(buffer.trim())
// Track the source location for source maps
if (source) {
node.src = [source, bufferStart, i]
node.dst = [source, bufferStart, i]
}
// Attach the rule to the parent in case it's nested.
if (parent) {
parent.nodes.push(node)
@ -434,6 +471,12 @@ export function parse(input: string) {
if (buffer.charCodeAt(0) === AT_SIGN) {
node = parseAtRule(buffer)
// Track the source location for source maps
if (source) {
node.src = [source, bufferStart, i]
node.dst = [source, bufferStart, i]
}
// At-rule is nested inside of a rule, attach it to the parent.
if (parent) {
parent.nodes.push(node)
@ -470,6 +513,11 @@ export function parse(input: string) {
let node = parseDeclaration(buffer, colonIdx)
if (!node) throw new Error(`Invalid declaration: \`${buffer.trim()}\``)
if (source) {
node.src = [source, bufferStart, i]
node.dst = [source, bufferStart, i]
}
parent.nodes.push(node)
}
}
@ -519,6 +567,8 @@ export function parse(input: string) {
continue
}
if (buffer === '') bufferStart = i
buffer += String.fromCharCode(currentChar)
}
}
@ -529,6 +579,12 @@ export function parse(input: string) {
if (buffer.charCodeAt(0) === AT_SIGN) {
let node = parseAtRule(buffer)
// Track the source location for source maps
if (source) {
node.src = [source, bufferStart, input.length]
node.dst = [source, bufferStart, input.length]
}
ast.push(node)
}

View file

@ -51,6 +51,7 @@ export const enum Polyfills {
type CompileOptions = {
base?: string
from?: string
polyfills?: Polyfills
loadModule?: (
id: string,
@ -136,6 +137,7 @@ async function parseCss(
ast: AstNode[],
{
base = '',
from,
loadModule = throwOnLoadModule,
loadStylesheet = throwOnLoadStylesheet,
}: CompileOptions = {},
@ -786,7 +788,7 @@ export async function compile(
features: Features
build(candidates: string[]): string
}> {
let ast = CSS.parse(css)
let ast = CSS.parse(css, { from: opts.from })
let api = await compileAst(ast, opts)
let compiledAst = ast
let compiledCss = css