mirror of
https://github.com/oven-sh/bun
synced 2026-02-10 02:48:50 +00:00
590 lines
23 KiB
Zig
590 lines
23 KiB
Zig
const std = @import("std");
|
|
const Allocator = std.mem.Allocator;
|
|
const bun = @import("bun");
|
|
const logger = bun.logger;
|
|
const Log = logger.Log;
|
|
|
|
pub const css = @import("./css_parser.zig");
|
|
pub const css_values = @import("./values/values.zig");
|
|
const DashedIdent = css_values.ident.DashedIdent;
|
|
const Ident = css_values.ident.Ident;
|
|
pub const Error = css.Error;
|
|
const Location = css.Location;
|
|
const PrintErr = css.PrintErr;
|
|
|
|
const ArrayList = std.ArrayListUnmanaged;
|
|
|
|
const sourcemap = @import("./sourcemap.zig");
|
|
|
|
/// Options that control how CSS is serialized to a string.
|
|
pub const PrinterOptions = struct {
|
|
/// Whether to minify the CSS, i.e. remove white space.
|
|
minify: bool = false,
|
|
/// An optional reference to a source map to write mappings into.
|
|
/// (Available when the `sourcemap` feature is enabled.)
|
|
source_map: ?*sourcemap.SourceMap = null,
|
|
/// An optional project root path, used to generate relative paths for sources used in CSS module hashes.
|
|
project_root: ?[]const u8 = null,
|
|
/// Targets to output the CSS for.
|
|
targets: Targets,
|
|
/// Whether to analyze dependencies (i.e. `@import` and `url()`).
|
|
/// If true, the dependencies are returned as part of the
|
|
/// [ToCssResult](super::stylesheet::ToCssResult).
|
|
///
|
|
/// When enabled, `@import` and `url()` dependencies
|
|
/// are replaced with hashed placeholders that can be replaced with the final
|
|
/// urls later (after bundling).
|
|
analyze_dependencies: ?css.dependencies.DependencyOptions = null,
|
|
/// A mapping of pseudo classes to replace with class names that can be applied
|
|
/// from JavaScript. Useful for polyfills, for example.
|
|
pseudo_classes: ?PseudoClasses = null,
|
|
public_path: []const u8 = "",
|
|
|
|
pub fn default() PrinterOptions {
|
|
return .{
|
|
.targets = Targets{
|
|
.browsers = null,
|
|
},
|
|
};
|
|
}
|
|
|
|
pub fn defaultWithMinify(minify: bool) PrinterOptions {
|
|
return .{
|
|
.targets = Targets{
|
|
.browsers = null,
|
|
},
|
|
.minify = minify,
|
|
};
|
|
}
|
|
};
|
|
|
|
/// A mapping of user action pseudo classes to replace with class names.
|
|
///
|
|
/// See [PrinterOptions](PrinterOptions).
|
|
const PseudoClasses = struct {
|
|
/// The class name to replace `:hover` with.
|
|
hover: ?[]const u8 = null,
|
|
/// The class name to replace `:active` with.
|
|
active: ?[]const u8 = null,
|
|
/// The class name to replace `:focus` with.
|
|
focus: ?[]const u8 = null,
|
|
/// The class name to replace `:focus-visible` with.
|
|
focus_visible: ?[]const u8 = null,
|
|
/// The class name to replace `:focus-within` with.
|
|
focus_within: ?[]const u8 = null,
|
|
};
|
|
|
|
pub const Targets = css.targets.Targets;
|
|
|
|
pub const Features = css.targets.Features;
|
|
|
|
const Browsers = css.targets.Browsers;
|
|
|
|
pub const ImportInfo = struct {
|
|
import_records: *const bun.BabyList(bun.ImportRecord),
|
|
/// bundle_v2.graph.ast.items(.url_for_css)
|
|
ast_urls_for_css: []const []const u8,
|
|
/// bundle_v2.graph.input_files.items(.unique_key_for_additional_file)
|
|
ast_unique_key_for_additional_file: []const []const u8,
|
|
|
|
/// Only safe to use when outside the bundler. As in, the import records
|
|
/// were not resolved to source indices. This will out-of-bounds otherwise.
|
|
pub fn initOutsideOfBundler(records: *bun.BabyList(bun.ImportRecord)) ImportInfo {
|
|
return .{
|
|
.import_records = records,
|
|
.ast_urls_for_css = &.{},
|
|
.ast_unique_key_for_additional_file = &.{},
|
|
};
|
|
}
|
|
};
|
|
|
|
/// A `Printer` represents a destination to output serialized CSS, as used in
|
|
/// the [ToCss](super::traits::ToCss) trait. It can wrap any destination that
|
|
/// implements [std::fmt::Write](std::fmt::Write), such as a [String](String).
|
|
///
|
|
/// A `Printer` keeps track of the current line and column position, and uses
|
|
/// this to generate a source map if provided in the options.
|
|
///
|
|
/// `Printer` also includes helper functions that assist with writing output
|
|
/// that respects options such as `minify`, and `css_modules`.
|
|
pub fn Printer(comptime Writer: type) type {
|
|
return struct {
|
|
// #[cfg(feature = "sourcemap")]
|
|
sources: ?*const ArrayList([]const u8),
|
|
dest: Writer,
|
|
loc: Location = Location{
|
|
.source_index = 0,
|
|
.line = 0,
|
|
.column = 1,
|
|
},
|
|
indent_amt: u8 = 0,
|
|
line: u32 = 0,
|
|
col: u32 = 0,
|
|
minify: bool,
|
|
targets: Targets,
|
|
vendor_prefix: css.VendorPrefix = .{},
|
|
in_calc: bool = false,
|
|
css_module: ?css.CssModule = null,
|
|
dependencies: ?ArrayList(css.Dependency) = null,
|
|
remove_imports: bool,
|
|
/// A mapping of pseudo classes to replace with class names that can be applied
|
|
/// from JavaScript. Useful for polyfills, for example.
|
|
pseudo_classes: ?PseudoClasses = null,
|
|
indentation_buf: std.ArrayList(u8),
|
|
ctx: ?*const css.StyleContext = null,
|
|
scratchbuf: std.ArrayList(u8),
|
|
error_kind: ?css.PrinterError = null,
|
|
import_info: ?ImportInfo = null,
|
|
public_path: []const u8,
|
|
symbols: *const bun.JSAst.Symbol.Map,
|
|
local_names: ?*const css.LocalsResultsMap = null,
|
|
/// NOTE This should be the same mimalloc heap arena allocator
|
|
allocator: Allocator,
|
|
// TODO: finish the fields
|
|
|
|
pub threadlocal var in_debug_fmt: if (bun.Environment.isDebug) bool else u0 = if (bun.Environment.isDebug) false else 0;
|
|
|
|
const This = @This();
|
|
|
|
pub fn lookupSymbol(this: *This, ref: bun.bundle_v2.Ref) []const u8 {
|
|
const symbols = this.symbols;
|
|
|
|
const final_ref = symbols.follow(ref);
|
|
if (this.local_names) |local_names| {
|
|
if (local_names.get(final_ref)) |local_name| return local_name;
|
|
}
|
|
|
|
const original_name = symbols.get(final_ref).?.original_name;
|
|
return original_name;
|
|
}
|
|
|
|
pub fn lookupIdentOrRef(this: *This, ident: css.css_values.ident.IdentOrRef) []const u8 {
|
|
if (comptime bun.Environment.isDebug) {
|
|
if (in_debug_fmt) {
|
|
return ident.debugIdent();
|
|
}
|
|
}
|
|
if (ident.isIdent()) {
|
|
return ident.asIdent().?.v;
|
|
}
|
|
return this.lookupSymbol(ident.asRef().?);
|
|
}
|
|
|
|
inline fn getWrittenAmt(writer: Writer) usize {
|
|
return switch (Writer) {
|
|
ArrayList(u8).Writer => writer.context.self.items.len,
|
|
*bun.js_printer.BufferWriter => writer.written.len,
|
|
else => @compileError("Dunno what to do with this type yo: " ++ @typeName(Writer)),
|
|
};
|
|
}
|
|
|
|
/// Returns the current source filename that is being printed.
|
|
pub fn filename(this: *const This) []const u8 {
|
|
if (this.sources) |sources| {
|
|
if (this.loc.source_index < sources.items.len) return sources.items[this.loc.source_index];
|
|
}
|
|
return "unknown.css";
|
|
}
|
|
|
|
/// Returns whether the indent level is greater than one.
|
|
pub fn isNested(this: *const This) bool {
|
|
return this.indent_amt > 2;
|
|
}
|
|
|
|
/// Add an error related to std lib fmt errors
|
|
pub fn addFmtError(this: *This) PrintErr {
|
|
this.error_kind = css.PrinterError{
|
|
.kind = .fmt_error,
|
|
.loc = null,
|
|
};
|
|
return PrintErr.CSSPrintError;
|
|
}
|
|
|
|
pub fn addNoImportRecordError(this: *This) PrintErr {
|
|
this.error_kind = css.PrinterError{
|
|
.kind = .no_import_records,
|
|
.loc = null,
|
|
};
|
|
return PrintErr.CSSPrintError;
|
|
}
|
|
|
|
pub fn addInvalidCssModulesPatternInGridError(this: *This) PrintErr {
|
|
this.error_kind = css.PrinterError{
|
|
.kind = .invalid_css_modules_pattern_in_grid,
|
|
.loc = css.ErrorLocation{
|
|
.filename = this.filename(),
|
|
.line = this.loc.line,
|
|
.column = this.loc.column,
|
|
},
|
|
};
|
|
return PrintErr.CSSPrintError;
|
|
}
|
|
|
|
/// Returns an error of the given kind at the provided location in the current source file.
|
|
pub fn newError(
|
|
this: *This,
|
|
kind: css.PrinterErrorKind,
|
|
maybe_loc: ?css.dependencies.Location,
|
|
) PrintErr!void {
|
|
bun.debugAssert(this.error_kind == null);
|
|
this.error_kind = css.PrinterError{
|
|
.kind = kind,
|
|
.loc = if (maybe_loc) |loc| css.ErrorLocation{
|
|
.filename = this.filename(),
|
|
.line = loc.line - 1,
|
|
.column = loc.column,
|
|
} else null,
|
|
};
|
|
return PrintErr.CSSPrintError;
|
|
}
|
|
|
|
pub fn deinit(this: *This) void {
|
|
this.scratchbuf.deinit();
|
|
this.indentation_buf.deinit();
|
|
if (this.dependencies) |*dependencies| {
|
|
dependencies.deinit(this.allocator);
|
|
}
|
|
}
|
|
|
|
/// If `import_records` is null, then the printer will error when it encounters code that relies on import records (urls())
|
|
pub fn new(
|
|
allocator: Allocator,
|
|
scratchbuf: std.ArrayList(u8),
|
|
dest: Writer,
|
|
options: PrinterOptions,
|
|
import_info: ?ImportInfo,
|
|
local_names: ?*const css.LocalsResultsMap,
|
|
symbols: *const bun.JSAst.Symbol.Map,
|
|
) This {
|
|
return .{
|
|
.sources = null,
|
|
.dest = dest,
|
|
.minify = options.minify,
|
|
.targets = options.targets,
|
|
.dependencies = if (options.analyze_dependencies != null) .empty else null,
|
|
.remove_imports = options.analyze_dependencies != null and options.analyze_dependencies.?.remove_imports,
|
|
.pseudo_classes = options.pseudo_classes,
|
|
.indentation_buf = .init(allocator),
|
|
.import_info = import_info,
|
|
.scratchbuf = scratchbuf,
|
|
.allocator = allocator,
|
|
.public_path = options.public_path,
|
|
.local_names = local_names,
|
|
.loc = .{
|
|
.source_index = 0,
|
|
.line = 0,
|
|
.column = 1,
|
|
},
|
|
.symbols = symbols,
|
|
};
|
|
}
|
|
|
|
pub inline fn getImportRecords(this: *This) PrintErr!*const bun.BabyList(bun.ImportRecord) {
|
|
if (this.import_info) |info| return info.import_records;
|
|
return this.addNoImportRecordError();
|
|
}
|
|
|
|
pub fn printImportRecord(this: *This, import_record_idx: u32) PrintErr!void {
|
|
if (this.import_info) |info| {
|
|
const import_record = info.import_records.at(import_record_idx);
|
|
const a, const b = bun.bundle_v2.cheapPrefixNormalizer(this.public_path, import_record.path.text);
|
|
try this.writeStr(a);
|
|
try this.writeStr(b);
|
|
return;
|
|
}
|
|
return this.addNoImportRecordError();
|
|
}
|
|
|
|
pub inline fn importRecord(this: *Printer(Writer), import_record_idx: u32) PrintErr!*const bun.ImportRecord {
|
|
if (this.import_info) |info| return info.import_records.at(import_record_idx);
|
|
return this.addNoImportRecordError();
|
|
}
|
|
|
|
pub inline fn getImportRecordUrl(this: *This, import_record_idx: u32) PrintErr![]const u8 {
|
|
const import_info = this.import_info orelse return this.addNoImportRecordError();
|
|
const record = import_info.import_records.at(import_record_idx);
|
|
if (record.source_index.isValid()) {
|
|
// It has an inlined url for CSS
|
|
const urls_for_css = import_info.ast_urls_for_css[record.source_index.get()];
|
|
if (urls_for_css.len > 0) {
|
|
return urls_for_css;
|
|
}
|
|
// It is a chunk URL
|
|
const unique_key_for_additional_file = import_info.ast_unique_key_for_additional_file[record.source_index.get()];
|
|
if (unique_key_for_additional_file.len > 0) {
|
|
return unique_key_for_additional_file;
|
|
}
|
|
}
|
|
// External URL stays as-is
|
|
return record.path.text;
|
|
}
|
|
|
|
pub fn context(this: *const Printer(Writer)) ?*const css.StyleContext {
|
|
return this.ctx;
|
|
}
|
|
|
|
/// To satisfy io.Writer interface
|
|
///
|
|
/// NOTE: Same constraints as `writeStr`, the `str` param is assumted to not
|
|
/// contain any newline characters
|
|
pub fn writeAll(this: *This, str: []const u8) !void {
|
|
return this.writeStr(str) catch std.mem.Allocator.Error.OutOfMemory;
|
|
}
|
|
|
|
pub fn writeComment(this: *This, comment: []const u8) PrintErr!void {
|
|
_ = this.dest.writeAll(comment) catch {
|
|
return this.addFmtError();
|
|
};
|
|
const new_lines = std.mem.count(u8, comment, "\n");
|
|
this.line += @intCast(new_lines);
|
|
this.col = 0;
|
|
const last_line_start = comment.len - (std.mem.lastIndexOfScalar(u8, comment, '\n') orelse comment.len);
|
|
this.col += @intCast(last_line_start);
|
|
return;
|
|
}
|
|
|
|
/// Writes a raw string to the underlying destination.
|
|
///
|
|
/// NOTE: Is is assumed that the string does not contain any newline characters.
|
|
/// If such a string is written, it will break source maps.
|
|
pub fn writeStr(this: *This, s: []const u8) PrintErr!void {
|
|
if (comptime bun.Environment.isDebug) {
|
|
bun.assert(std.mem.indexOfScalar(u8, s, '\n') == null);
|
|
}
|
|
this.col += @intCast(s.len);
|
|
_ = this.dest.writeAll(s) catch {
|
|
return this.addFmtError();
|
|
};
|
|
return;
|
|
}
|
|
|
|
/// Writes a formatted string to the underlying destination.
|
|
///
|
|
/// NOTE: Is is assumed that the formatted string does not contain any newline characters.
|
|
/// If such a string is written, it will break source maps.
|
|
pub fn writeFmt(this: *This, comptime fmt: []const u8, args: anytype) PrintErr!void {
|
|
// assuming the writer comes from an ArrayList
|
|
const start: usize = getWrittenAmt(this.dest);
|
|
this.dest.print(fmt, args) catch bun.outOfMemory();
|
|
const written = getWrittenAmt(this.dest) - start;
|
|
this.col += @intCast(written);
|
|
}
|
|
|
|
fn replaceDots(allocator: Allocator, s: []const u8) []const u8 {
|
|
var str = allocator.dupe(u8, s) catch bun.outOfMemory();
|
|
std.mem.replaceScalar(u8, str[0..], '.', '-');
|
|
return str;
|
|
}
|
|
|
|
pub fn writeIdentOrRef(this: *This, ident: css.css_values.ident.IdentOrRef, handle_css_module: bool) PrintErr!void {
|
|
if (!handle_css_module) {
|
|
if (ident.asIdent()) |identifier| {
|
|
return css.serializer.serializeIdentifier(identifier.v, this) catch return this.addFmtError();
|
|
} else {
|
|
const ref = ident.asRef().?;
|
|
const symbol = this.symbols.get(ref) orelse return this.addFmtError();
|
|
return css.serializer.serializeIdentifier(symbol.original_name, this) catch return this.addFmtError();
|
|
}
|
|
}
|
|
|
|
const str = this.lookupIdentOrRef(ident);
|
|
return css.serializer.serializeIdentifier(str, this) catch return this.addFmtError();
|
|
}
|
|
|
|
/// Writes a CSS identifier to the underlying destination, escaping it
|
|
/// as appropriate. If the `css_modules` option was enabled, then a hash
|
|
/// is added, and the mapping is added to the CSS module.
|
|
pub fn writeIdent(this: *This, ident: []const u8, handle_css_module: bool) PrintErr!void {
|
|
if (handle_css_module) {
|
|
if (this.css_module) |*css_module| {
|
|
const Closure = struct { first: bool, printer: *This };
|
|
var closure = Closure{ .first = true, .printer = this };
|
|
css_module.config.pattern.write(
|
|
css_module.hashes.items[this.loc.source_index],
|
|
css_module.sources.items[this.loc.source_index],
|
|
ident,
|
|
&closure,
|
|
struct {
|
|
pub fn writeFn(self: *Closure, s1: []const u8, replace_dots: bool) void {
|
|
// PERF: stack fallback?
|
|
const s = if (!replace_dots) s1 else replaceDots(self.printer.allocator, s1);
|
|
defer if (replace_dots) self.printer.allocator.free(s);
|
|
self.printer.col += @intCast(s.len);
|
|
if (self.first) {
|
|
self.first = false;
|
|
return css.serializer.serializeIdentifier(s, self.printer) catch |e| css.OOM(e);
|
|
} else {
|
|
return css.serializer.serializeName(s, self.printer) catch |e| css.OOM(e);
|
|
}
|
|
}
|
|
}.writeFn,
|
|
);
|
|
|
|
css_module.addLocal(this.allocator, ident, ident, this.loc.source_index);
|
|
return;
|
|
}
|
|
}
|
|
|
|
return css.serializer.serializeIdentifier(ident, this) catch return this.addFmtError();
|
|
}
|
|
|
|
pub fn writeDashedIdent(this: *This, ident: *const DashedIdent, is_declaration: bool) !void {
|
|
try this.writeStr("--");
|
|
|
|
if (this.css_module) |*css_module| {
|
|
if (css_module.config.dashed_idents) {
|
|
const Fn = struct {
|
|
pub fn writeFn(self: *This, s1: []const u8, replace_dots: bool) void {
|
|
const s = if (!replace_dots) s1 else replaceDots(self.allocator, s1);
|
|
defer if (replace_dots) self.allocator.free(s);
|
|
self.col += @intCast(s.len);
|
|
return css.serializer.serializeName(s, self) catch |e| css.OOM(e);
|
|
}
|
|
};
|
|
css_module.config.pattern.write(
|
|
css_module.hashes.items[this.loc.source_index],
|
|
css_module.sources.items[this.loc.source_index],
|
|
ident.v[2..],
|
|
this,
|
|
Fn.writeFn,
|
|
);
|
|
|
|
if (is_declaration) {
|
|
css_module.addDashed(this.allocator, ident.v, this.loc.source_index);
|
|
}
|
|
}
|
|
}
|
|
|
|
return css.serializer.serializeName(ident.v[2..], this) catch return this.addFmtError();
|
|
}
|
|
|
|
pub fn writeByte(this: *This, char: u8) !void {
|
|
return this.writeChar(char) catch return Allocator.Error.OutOfMemory;
|
|
}
|
|
|
|
/// Write a single character to the underlying destination.
|
|
pub fn writeChar(this: *This, char: u8) PrintErr!void {
|
|
if (char == '\n') {
|
|
this.line += 1;
|
|
this.col = 0;
|
|
} else {
|
|
this.col += 1;
|
|
}
|
|
_ = this.dest.writeByte(char) catch {
|
|
return this.addFmtError();
|
|
};
|
|
}
|
|
|
|
/// Writes a newline character followed by indentation.
|
|
/// If the `minify` option is enabled, then nothing is printed.
|
|
pub fn newline(this: *This) PrintErr!void {
|
|
if (this.minify) {
|
|
return;
|
|
}
|
|
|
|
try this.writeChar('\n');
|
|
return this.writeIndent();
|
|
}
|
|
|
|
/// Writes a delimiter character, followed by whitespace (depending on the `minify` option).
|
|
/// If `ws_before` is true, then whitespace is also written before the delimiter.
|
|
pub fn delim(this: *This, delim_: u8, ws_before: bool) PrintErr!void {
|
|
if (ws_before) {
|
|
try this.whitespace();
|
|
}
|
|
try this.writeChar(delim_);
|
|
return this.whitespace();
|
|
}
|
|
|
|
/// Writes a single whitespace character, unless the `minify` option is enabled.
|
|
///
|
|
/// Use `write_char` instead if you wish to force a space character to be written,
|
|
/// regardless of the `minify` option.
|
|
pub fn whitespace(this: *This) PrintErr!void {
|
|
if (this.minify) return;
|
|
return this.writeChar(' ');
|
|
}
|
|
|
|
pub fn withContext(
|
|
this: *This,
|
|
selectors: *const css.SelectorList,
|
|
closure: anytype,
|
|
comptime func: anytype,
|
|
) PrintErr!void {
|
|
const parent = if (this.ctx) |ctx| parent: {
|
|
this.ctx = null;
|
|
break :parent ctx;
|
|
} else null;
|
|
|
|
const ctx = css.StyleContext{ .selectors = selectors, .parent = parent };
|
|
|
|
this.ctx = &ctx;
|
|
const res = func(closure, Writer, this);
|
|
this.ctx = parent;
|
|
|
|
return res;
|
|
}
|
|
|
|
pub fn withClearedContext(
|
|
this: *This,
|
|
closure: anytype,
|
|
comptime func: anytype,
|
|
) PrintErr!void {
|
|
const parent = if (this.ctx) |ctx| parent: {
|
|
this.ctx = null;
|
|
break :parent ctx;
|
|
} else null;
|
|
const res = func(closure, Writer, this);
|
|
this.ctx = parent;
|
|
return res;
|
|
}
|
|
|
|
/// Increases the current indent level.
|
|
pub fn indent(this: *This) void {
|
|
this.indent_amt += 2;
|
|
}
|
|
|
|
/// Decreases the current indent level.
|
|
pub fn dedent(this: *This) void {
|
|
this.indent_amt -= 2;
|
|
}
|
|
|
|
const INDENTS: []const []const u8 = indents: {
|
|
const levels = 32;
|
|
var indents: [levels][]const u8 = undefined;
|
|
for (0..levels) |i| {
|
|
const n = i * 2;
|
|
var str: [n]u8 = undefined;
|
|
for (0..n) |j| {
|
|
str[j] = ' ';
|
|
}
|
|
indents[i] = str;
|
|
}
|
|
break :indents indents;
|
|
};
|
|
|
|
fn getIndent(this: *This, idnt: u8) []const u8 {
|
|
// divide by 2 to get index into table
|
|
const i = idnt >> 1;
|
|
// PERF: may be faster to just do `i < (IDENTS.len - 1) * 2` (e.g. 62 if IDENTS.len == 32) here
|
|
if (i < INDENTS.len) {
|
|
return INDENTS[i];
|
|
}
|
|
if (this.indentation_buf.items.len < idnt) {
|
|
this.indentation_buf.appendNTimes(' ', this.indentation_buf.items.len - idnt) catch unreachable;
|
|
} else {
|
|
this.indentation_buf.items = this.indentation_buf.items[0..idnt];
|
|
}
|
|
return this.indentation_buf.items;
|
|
}
|
|
|
|
fn writeIndent(this: *This) PrintErr!void {
|
|
bun.debugAssert(!this.minify);
|
|
if (this.indent_amt > 0) {
|
|
// try this.writeStr(this.getIndent(this.ident));
|
|
this.dest.writeByteNTimes(' ', this.indent_amt) catch return this.addFmtError();
|
|
}
|
|
}
|
|
};
|
|
}
|