@tbela99/css-parser
Version:
CSS parser, minifier and validator for node and the browser
346 lines (343 loc) • 10.6 kB
JavaScript
import process from 'node:process';
import { deprecate } from 'node:util';
import { Readable } from 'node:stream';
import { createReadStream } from 'node:fs';
import { lstat, readFile } from 'node:fs/promises';
import { doParse } from './lib/parser/parse.js';
export { parseDeclarations, parseString } from './lib/parser/parse.js';
import { doRender } from './lib/renderer/render.js';
export { renderValue as renderToken } from './lib/renderer/render.js';
import { ModuleScopeEnumOptions } from './lib/ast/types.js';
export { ColorType, EnumAstNodeStatus, EnumToken, ModuleCaseTransformEnum, ValidationLevel } from './lib/ast/types.js';
import { tokenizeStream, tokenize } from './lib/parser/tokenize.js';
import { dirname, resolve, matchUrl } from './lib/fs/resolve.js';
import { ResponseType } from './types.js';
export { minify } from './lib/ast/minify.js';
export { expand } from './lib/ast/expand.js';
export { WalkerEvent, WalkerOptionEnum, walk, walkValues } from './lib/ast/walk.js';
export { convertColor } from './lib/syntax/color/color.js';
export { isOkLabClose, okLabDistance } from './lib/syntax/color/utils/distance.js';
export { find, findAll, findByValue, findLast } from './lib/ast/find.js';
export { cloneNode } from './lib/ast/clone.js';
export { replaceNodeOrValue } from './lib/parser/utils/token.js';
export { SourceMap } from './lib/renderer/sourcemap/sourcemap.js';
export { FeatureWalkMode } from './lib/ast/features/type.js';
/**
* Load file or url
* @param url
* @param currentDirectory
* @param responseType
* @throws Error file not found
*
* ```ts
* import {load, ResponseType} from '@tbela99/css-parser';
* const result = await load(file, '.', ResponseType.ArrayBuffer) as ArrayBuffer;
* ```
*/
async function load(url, currentDirectory = ".", responseType = false) {
const resolved = typeof url == "string" ? resolve(url, currentDirectory) : url;
if (typeof responseType == "boolean") {
responseType = responseType ? ResponseType.ReadableStream : ResponseType.Text;
}
if (matchUrl.test(resolved.absolute)) {
return fetch(resolved.absolute).then(async (response) => {
if (!response.ok) {
throw new Error(`${response.status} ${response.statusText} ${response.url}`);
}
if (responseType == ResponseType.ArrayBuffer) {
return response.arrayBuffer();
}
return responseType == ResponseType.ReadableStream
? response.body
: response.text();
});
}
try {
const stats = await lstat(resolved.absolute);
if (stats.isFile()) {
if (responseType == ResponseType.Text) {
return readFile(resolved.absolute, "utf-8");
}
if (responseType == ResponseType.ArrayBuffer) {
return readFile(resolved.absolute).then((buffer) => buffer.buffer);
}
return Readable.toWeb(createReadStream(resolved.absolute, {
encoding: "utf-8",
highWaterMark: 64 * 1024,
}));
}
}
catch (error) {
console.warn(error);
}
throw new Error(`File not found: '${resolved.absolute || url}'`);
}
/**
* Render the ast tree
* @param data
* @param options
* @param mapping
*
* Example:
*
* ```ts
*
* import {render, ColorType} from '@tbela99/css-parser';
*
* const css = 'body { color: color(from hsl(0 100% 50%) xyz x y z); }';
* const parseResult = await parse(css);
*
* let renderResult = render(parseResult.ast);
* console.log(result.code);
*
* // body{color:red}
*
*
* renderResult = render(parseResult.ast, {beautify: true, convertColor: ColorType.SRGB});
* console.log(renderResult.code);
*
* // body {
* // color: color(srgb 1 0 0)
* // }
* ```
*/
function render(data, options = {}, mapping) {
return doRender(data, Object.assign(options, { resolve, dirname, cwd: options.cwd ?? process.cwd() }), mapping);
}
/**
* Parse css file
* @param file url or path
* @param options
* @param asStream load file as stream
*
* @deprecated
* @see {@link parse}
* @throws Error file not found
*
* Example:
*
* ```ts
*
* import {parseFile} from '@tbela99/css-parser';
*
* // remote file
* let result = await parseFile('https://docs.deno.com/styles.css');
* console.log(result.ast);
*
* // local file
* result = await parseFile('./css/styles.css');
* console.log(result.ast);
* ```
*/
const parseFile = deprecate(async (file, options = {}, asStream = false) => parse({ file, asStream, ...options }), "parseFile is deprecated, use parse instead as parse({file, asStream, ...options})");
/**
* Parse css
* @param stream
* @param options
*
* @throws Error file not found
*
* Parsing a string
*
* ```ts
*
* import {parse} from '@tbela99/css-parser';
*
* // css string
* let result = await parse(css);
* console.log(result.ast);
* ```
*
* Parsing a Readable stream
*
* ```ts
*
* import {parse} from '@tbela99/css-parser';
* import {Readable} from "node:stream";
*
* // usage: node index.ts < styles.css or cat styles.css | node index.ts
*
* const readableStream = Readable.toWeb(process.stdin);
* let result = await parse(readableStream, {beautify: true});
*
* console.log(result.ast);
* ```
*
* Parsing a file as a ReadableStream
*
* ```ts
*
* import {parse} from '@tbela99/css-parser';
*
* const response = await fetch('https://docs.deno.com/styles.css');
* const result = await parse(response.body, {beautify: true});
*
* console.log(result.ast);
* ```
*/
async function parse(...args) {
let options;
let stream;
if (typeof args[0] === "string" || args[0] instanceof ReadableStream) {
stream = args[0];
options = args[1];
}
else {
// @ts-expect-error
const { file, input, ...opt } = args[0];
options = opt;
if (file != null) {
return Promise.resolve((options.load ?? load)(file, "", options.asStream ?? false)).then((stream) => parse(stream, { src: file, ...options }));
}
else {
stream = input;
}
}
options ??= {};
options.src ??= "";
Object.assign(options, {
load,
resolve,
dirname,
cwd: options.cwd ?? process.cwd(),
});
options.src = resolve(options.src, options.cwd).relative;
options.parseInfo = {
stream,
buffer: "",
src: options.src ?? "",
offset: 0,
time: 0,
position: { ind: 0, lin: 1, col: 0 },
currentPosition: { ind: -1, lin: 1, col: 0 },
};
return doParse(stream instanceof ReadableStream ? tokenizeStream(stream, options.parseInfo) : tokenize(options.parseInfo), options).then((result) => {
const { revMapping, ...res } = result;
return res;
});
}
/**
* Transform css file
* @param file url or path
* @param options
* @param asStream load file as stream
*
* @deprecated Use transform() instead.
* @throws Error file not found
*
* Example:
*
* ```ts
*
* import {transform} from '@tbela99/css-parser';
*
* // remote file
* let result = await transform({file: 'https://docs.deno.com/styles.css'});
* console.log(result.code);
*
* // local file
* result = await transform({file: './css/styles.css'});
* console.log(result.code);
* ```
*/
const transformFile = deprecate(async (file, options = {}, asStream = false) => transform({
file,
asStream,
...options,
}), "transformFile is deprecated, use transform instead as transform({file, asStream, ...options})");
/**
* Transform css
* @param css
* @param options
*
* Paarsing a string
*
* ```ts
*
* import {transform} from '@tbela99/css-parser';
*
* // css string
* const result = await transform(css);
* console.log(result.code);
* ```
*
* Parsing a Readable stream
*
* ```ts
*
* import {transform} from '@tbela99/css-parser';
* import {Readable} from "node:stream";
*
* // usage: node index.ts < styles.css or cat styles.css | node index.ts
*
* const readableStream = Readable.toWeb(process.stdin);
* const result = await transform(readableStream, {beautify: true});
*
* console.log(result.code);
* ```
*
* Example using fetch
*
* ```ts
*
* import {transform} from '@tbela99/css-parser';
*
* const response = await fetch('https://docs.deno.com/styles.css');
* result = await transform(response.body, {beautify: true});
*
* console.log(result.code);
* ```
*/
async function transform(...args) {
let options;
let stream;
if (typeof args[0] === "string" || args[0] instanceof ReadableStream) {
stream = args[0];
options = args[1];
}
else {
// @ts-expect-error
const { file, input, ...opt } = args[0];
options = opt;
if (file != null) {
return Promise.resolve((options.load ?? load)(file, "", options.asStream ?? false)).then((stream) => transform(stream, { src: file, ...options }));
}
else {
stream = input;
}
}
options ??= {};
options = { minify: true, removeEmpty: true, removeCharset: true, ...options };
const startTime = performance.now();
return parse(stream, options).then((parseResult) => {
let mapping = null;
let importMapping = null;
if (typeof options.module == "number" && options.module & ModuleScopeEnumOptions.ICSS) {
mapping = parseResult.mapping;
importMapping = parseResult.importMapping;
}
else if (typeof options.module == "object" &&
typeof options.module.scoped == "number" &&
options.module.scoped & ModuleScopeEnumOptions.ICSS) {
mapping = parseResult.mapping;
importMapping = parseResult.importMapping;
}
// ast already expanded by parse
const rendered = render(parseResult.ast, {
...options,
expandNestingRules: false,
}, mapping != null ? { mapping, importMapping } : null);
return {
...parseResult,
...rendered,
errors: parseResult.errors.concat(rendered.errors),
stats: {
bytesOut: rendered.code.length,
...parseResult.stats,
render: rendered.stats.total,
total: `${(performance.now() - startTime).toFixed(2)}ms`,
},
};
});
}
export { ModuleScopeEnumOptions, ResponseType, dirname, load, parse, parseFile, render, resolve, transform, transformFile };