Files
bun.sh/src/css/selectors/selector.zig
taylor.fish 41b1efe12c Rename disabled parameter in Output.scoped (#21769)
It's very confusing.

(For internal tracking: fixes STAB-977)
2025-08-11 20:19:34 -07:00

1626 lines
68 KiB
Zig

pub const css = @import("../css_parser.zig");
const CSSString = css.CSSString;
const CSSStringFns = css.CSSStringFns;
pub const Printer = css.Printer;
pub const PrintErr = css.PrintErr;
pub const Selector = parser.Selector;
pub const SelectorList = parser.SelectorList;
pub const Component = parser.Component;
pub const PseudoClass = parser.PseudoClass;
pub const PseudoElement = parser.PseudoElement;
const debug = bun.Output.scoped(.CSS_SELECTORS, .visible);
/// Our implementation of the `SelectorImpl` interface
///
pub const impl = struct {
pub const Selectors = struct {
pub const SelectorImpl = struct {
pub const AttrValue = css.css_values.string.CSSString;
pub const Identifier = css.css_values.ident.Ident;
/// An identifier which could be a local name for use in CSS modules
pub const LocalIdentifier = css.css_values.ident.IdentOrRef;
pub const LocalName = css.css_values.ident.Ident;
pub const NamespacePrefix = css.css_values.ident.Ident;
pub const NamespaceUrl = []const u8;
pub const BorrowedNamespaceUrl = []const u8;
pub const BorrowedLocalName = css.css_values.ident.Ident;
pub const NonTSPseudoClass = parser.PseudoClass;
pub const PseudoElement = parser.PseudoElement;
pub const VendorPrefix = css.VendorPrefix;
pub const ExtraMatchingData = void;
};
pub const LocalIdentifier = struct {
pub fn fromIdent(ident: css.css_values.ident.Ident) SelectorImpl.LocalIdentifier {
return .{ .v = ident };
}
};
};
};
pub const parser = @import("./parser.zig");
/// Returns whether two selector lists are equivalent, i.e. the same minus any vendor prefix differences.
pub fn isEquivalent(selectors: []const Selector, other: []const Selector) bool {
if (selectors.len != other.len) return false;
for (selectors, 0..) |*a, i| {
const b = &other[i];
if (a.len() != b.len()) return false;
for (a.components.items, b.components.items) |*a_comp, *b_comp| {
const is_equivalent = blk: {
if (a_comp.* == .non_ts_pseudo_class and b_comp.* == .non_ts_pseudo_class) {
break :blk a_comp.non_ts_pseudo_class.isEquivalent(&b_comp.non_ts_pseudo_class);
} else if (a_comp.* == .pseudo_element and b_comp.* == .pseudo_element) {
break :blk a_comp.pseudo_element.isEquivalent(&b_comp.pseudo_element);
} else if ((a_comp.* == .any and b_comp.* == .is) or
(a_comp.* == .is and b_comp.* == .any) or
(a_comp.* == .any and b_comp.* == .any) or
(a_comp.* == .is and b_comp.* == .is))
{
const a_selectors = switch (a_comp.*) {
.any => |v| v.selectors,
.is => |v| v,
else => unreachable,
};
const b_selectors = switch (b_comp.*) {
.any => |v| v.selectors,
.is => |v| v,
else => unreachable,
};
break :blk isEquivalent(a_selectors, b_selectors);
} else {
break :blk Component.eql(a_comp, b_comp);
}
};
if (!is_equivalent) {
return false;
}
}
}
return true;
}
/// Downlevels the given selectors to be compatible with the given browser targets.
/// Returns the necessary vendor prefixes.
pub fn downlevelSelectors(allocator: Allocator, selectors: []Selector, targets: css.targets.Targets) css.VendorPrefix {
var necessary_prefixes = css.VendorPrefix{};
for (selectors) |*selector| {
for (selector.components.items) |*component| {
bun.bits.insert(css.VendorPrefix, &necessary_prefixes, downlevelComponent(allocator, component, targets));
}
}
return necessary_prefixes;
}
pub fn downlevelComponent(allocator: Allocator, component: *Component, targets: css.targets.Targets) css.VendorPrefix {
return switch (component.*) {
.non_ts_pseudo_class => |*pc| {
return switch (pc.*) {
.dir => |*d| {
if (targets.shouldCompileSame(.dir_selector)) {
component.* = downlevelDir(allocator, d.direction, targets);
return downlevelComponent(allocator, component, targets);
}
return css.VendorPrefix{};
},
.lang => |l| {
// :lang() with multiple languages is not supported everywhere.
// compile this to :is(:lang(a), :lang(b)) etc.
if (l.languages.items.len > 1 and targets.shouldCompileSame(.lang_selector_list)) {
component.* = .{ .is = langListToSelectors(allocator, l.languages.items) };
return downlevelComponent(allocator, component, targets);
}
return css.VendorPrefix{};
},
else => pc.getNecessaryPrefixes(targets),
};
},
.pseudo_element => |*pe| pe.getNecessaryPrefixes(targets),
.is => |selectors| {
var necessary_prefixes = downlevelSelectors(allocator, selectors, targets);
// Convert :is to :-webkit-any/:-moz-any if needed.
// All selectors must be simple, no combinators are supported.
if (targets.shouldCompileSame(.is_selector) and
!shouldUnwrapIs(selectors) and brk: {
for (selectors) |*selector| {
if (selector.hasCombinator()) break :brk false;
}
break :brk true;
}) {
bun.bits.insert(css.VendorPrefix, &necessary_prefixes, targets.prefixes(css.VendorPrefix{ .none = true }, .any_pseudo));
} else {
necessary_prefixes.none = true;
}
return necessary_prefixes;
},
.negation => |selectors| {
var necessary_prefixes = downlevelSelectors(allocator, selectors, targets);
// Downlevel :not(.a, .b) -> :not(:is(.a, .b)) if not list is unsupported.
// We need to use :is() / :-webkit-any() rather than :not(.a):not(.b) to ensure the specificity is equivalent.
// https://drafts.csswg.org/selectors/#specificity-rules
if (selectors.len > 1 and css.targets.Targets.shouldCompileSame(&targets, .not_selector_list)) {
const is: Selector = Selector.fromComponent(allocator, Component{ .is = selectors: {
const new_selectors = allocator.alloc(Selector, selectors.len) catch bun.outOfMemory();
for (new_selectors, selectors) |*new, *sel| {
new.* = sel.deepClone(allocator);
}
break :selectors new_selectors;
} });
var list = ArrayList(Selector).initCapacity(allocator, 1) catch bun.outOfMemory();
list.appendAssumeCapacity(is);
component.* = .{ .negation = list.items };
if (targets.shouldCompileSame(.is_selector)) {
bun.bits.insert(css.VendorPrefix, &necessary_prefixes, targets.prefixes(css.VendorPrefix{ .none = true }, .any_pseudo));
} else {
bun.bits.insert(css.VendorPrefix, &necessary_prefixes, css.VendorPrefix{ .none = true });
}
}
return necessary_prefixes;
},
.where, .has => |s| downlevelSelectors(allocator, s, targets),
.any => |*a| downlevelSelectors(allocator, a.selectors, targets),
else => css.VendorPrefix{},
};
}
const RTL_LANGS: []const []const u8 = &.{
"ae", "ar", "arc", "bcc", "bqi", "ckb", "dv", "fa", "glk", "he", "ku", "mzn", "nqo", "pnb", "ps", "sd", "ug",
"ur", "yi",
};
fn downlevelDir(allocator: Allocator, dir: parser.Direction, targets: css.targets.Targets) Component {
// Convert :dir to :lang. If supported, use a list of languages in a single :lang,
// otherwise, use :is/:not, which may be further downleveled to e.g. :-webkit-any.
if (!targets.shouldCompileSame(.lang_selector_list)) {
const c = Component{
.non_ts_pseudo_class = PseudoClass{
.lang = .{ .languages = lang: {
var list = ArrayList([]const u8).initCapacity(allocator, RTL_LANGS.len) catch bun.outOfMemory();
list.appendSliceAssumeCapacity(RTL_LANGS);
break :lang list;
} },
},
};
if (dir == .ltr) return Component{
.negation = negation: {
var list = allocator.alloc(Selector, 1) catch bun.outOfMemory();
list[0] = Selector.fromComponent(allocator, c);
break :negation list;
},
};
return c;
} else {
if (dir == .ltr) return Component{ .negation = langListToSelectors(allocator, RTL_LANGS) };
return Component{ .is = langListToSelectors(allocator, RTL_LANGS) };
}
}
fn langListToSelectors(allocator: Allocator, langs: []const []const u8) []Selector {
var selectors = allocator.alloc(Selector, langs.len) catch bun.outOfMemory();
for (langs, selectors[0..]) |lang, *sel| {
sel.* = Selector.fromComponent(allocator, Component{
.non_ts_pseudo_class = PseudoClass{
.lang = .{ .languages = langs: {
var list = ArrayList([]const u8).initCapacity(allocator, 1) catch bun.outOfMemory();
list.appendAssumeCapacity(lang);
break :langs list;
} },
},
});
}
return selectors;
}
/// Returns the vendor prefix (if any) used in the given selector list.
/// If multiple vendor prefixes are seen, this is invalid, and an empty result is returned.
pub fn getPrefix(selectors: *const SelectorList) css.VendorPrefix {
var prefix = css.VendorPrefix{};
for (selectors.v.slice()) |*selector| {
for (selector.components.items) |*component_| {
const component: *const Component = component_;
const p = switch (component.*) {
// Return none rather than empty for these so that we call downlevel_selectors.
.non_ts_pseudo_class => |*pc| switch (pc.*) {
.lang => css.VendorPrefix{ .none = true },
.dir => css.VendorPrefix{ .none = true },
else => pc.getPrefix(),
},
.is => css.VendorPrefix{ .none = true },
.where => css.VendorPrefix{ .none = true },
.has => css.VendorPrefix{ .none = true },
.negation => css.VendorPrefix{ .none = true },
.any => |*any| any.vendor_prefix,
.pseudo_element => |*pe| pe.getPrefix(),
else => css.VendorPrefix{},
};
if (!p.isEmpty()) {
// Allow none to be mixed with a prefix.
var prefix_without_none = prefix;
prefix_without_none.none = false;
if (prefix_without_none.isEmpty() or prefix_without_none == p) {
bun.bits.insert(css.VendorPrefix, &prefix, p);
} else {
return css.VendorPrefix{};
}
}
}
}
return prefix;
}
pub fn isCompatible(selectors: []const parser.Selector, targets: css.targets.Targets) bool {
const F = css.compat.Feature;
for (selectors) |*selector| {
for (selector.components.items) |*component| {
const feature = switch (component.*) {
.id, .class, .local_name => continue,
.explicit_any_namespace,
.explicit_no_namespace,
.default_namespace,
.namespace,
=> F.namespaces,
.explicit_universal_type => F.selectors2,
.attribute_in_no_namespace_exists => F.selectors2,
.attribute_in_no_namespace => |x| brk: {
if (x.case_sensitivity != parser.attrs.ParsedCaseSensitivity.case_sensitive) break :brk F.case_insensitive;
break :brk switch (x.operator) {
.equal, .includes, .dash_match => F.selectors2,
.prefix, .substring, .suffix => F.selectors3,
};
},
.attribute_other => |attr| switch (attr.operation) {
.exists => F.selectors2,
.with_value => |*x| brk: {
if (x.case_sensitivity != parser.attrs.ParsedCaseSensitivity.case_sensitive) break :brk F.case_insensitive;
break :brk switch (x.operator) {
.equal, .includes, .dash_match => F.selectors2,
.prefix, .substring, .suffix => F.selectors3,
};
},
},
.empty, .root => F.selectors3,
.negation => |sels| {
// :not() selector list is not forgiving.
if (!targets.isCompatible(F.selectors3) or !isCompatible(sels, targets)) return false;
continue;
},
.nth => |*data| brk: {
if (data.ty == .child and data.a == 0 and data.b == 1) break :brk F.selectors2;
if (data.ty == .col or data.ty == .last_col) return false;
break :brk F.selectors3;
},
.nth_of => |*n| {
if (!targets.isCompatible(F.nth_child_of) or !isCompatible(n.selectors, targets)) return false;
continue;
},
// These support forgiving selector lists, so no need to check nested selectors.
.is => |sels| brk: {
// ... except if we are going to unwrap them.
if (shouldUnwrapIs(sels) and isCompatible(sels, targets)) continue;
break :brk F.is_selector;
},
.where, .nesting => F.is_selector,
.any => return false,
.has => |sels| {
if (!targets.isCompatible(F.has_selector) or !isCompatible(sels, targets)) return false;
continue;
},
.scope, .host, .slotted => F.shadowdomv1,
.part => F.part_pseudo,
.non_ts_pseudo_class => |*pseudo| brk: {
switch (pseudo.*) {
.link, .visited, .active, .hover, .focus, .lang => break :brk F.selectors2,
.checked, .disabled, .enabled, .target => break :brk F.selectors3,
.any_link => |prefix| {
if (prefix == css.VendorPrefix{ .none = true }) break :brk F.any_link;
},
.indeterminate => break :brk F.indeterminate_pseudo,
.fullscreen => |prefix| {
if (prefix == css.VendorPrefix{ .none = true }) break :brk F.fullscreen;
},
.focus_visible => break :brk F.focus_visible,
.focus_within => break :brk F.focus_within,
.default => break :brk F.default_pseudo,
.dir => break :brk F.dir_selector,
.optional => break :brk F.optional_pseudo,
.placeholder_shown => |prefix| {
if (prefix == css.VendorPrefix{ .none = true }) break :brk F.placeholder_shown;
},
inline .read_only, .read_write => |prefix| {
if (prefix == css.VendorPrefix{ .none = true }) break :brk F.read_only_write;
},
.valid, .invalid, .required => break :brk F.form_validation,
.in_range, .out_of_range => break :brk F.in_out_of_range,
.autofill => |prefix| {
if (prefix == css.VendorPrefix{ .none = true }) break :brk F.autofill;
},
// Experimental, no browser support.
.current,
.past,
.future,
.playing,
.paused,
.seeking,
.stalled,
.buffering,
.muted,
.volume_locked,
.target_within,
.local_link,
.blank,
.user_invalid,
.user_valid,
.defined,
=> return false,
.custom => {},
else => {},
}
return false;
},
.pseudo_element => |*pseudo| brk: {
switch (pseudo.*) {
.after, .before => break :brk F.gencontent,
.first_line => break :brk F.first_line,
.first_letter => break :brk F.first_letter,
.selection => |prefix| {
if (prefix == css.VendorPrefix{ .none = true }) break :brk F.selection;
},
.placeholder => |prefix| {
if (prefix == css.VendorPrefix{ .none = true }) break :brk F.placeholder;
},
.marker => break :brk F.marker_pseudo,
.backdrop => |prefix| {
if (prefix == css.VendorPrefix{ .none = true }) break :brk F.dialog;
},
.cue => break :brk F.cue,
.cue_function => break :brk F.cue_function,
.custom => return false,
else => {},
}
return false;
},
.combinator => |*combinator| brk: {
break :brk switch (combinator.*) {
.child, .next_sibling => F.selectors2,
.later_sibling => F.selectors3,
else => continue,
};
},
};
if (!targets.isCompatible(feature)) return false;
}
}
return true;
}
/// Determines whether a selector list contains only unused selectors.
/// A selector is considered unused if it contains a class or id component that exists in the set of unused symbols.
pub fn isUnused(
selectors: []const parser.Selector,
unused_symbols: *const std.StringArrayHashMapUnmanaged(void),
symbols: *const css.SymbolList,
parent_is_unused: bool,
) bool {
if (unused_symbols.count() == 0) return false;
for (selectors) |*selector| {
if (!isSelectorUnused(selector, unused_symbols, symbols, parent_is_unused)) return false;
}
return true;
}
fn isSelectorUnused(
selector: *const parser.Selector,
unused_symbols: *const std.StringArrayHashMapUnmanaged(void),
symbols: *const css.SymbolList,
parent_is_unused: bool,
) bool {
for (selector.components.items) |*component| {
switch (component.*) {
.class, .id => |ident| {
const actual_ident = ident.asOriginalString(symbols);
if (unused_symbols.contains(actual_ident)) return true;
},
.is, .where => |is| {
if (isUnused(is, unused_symbols, symbols, parent_is_unused)) return true;
},
.any => |any| {
if (isUnused(any.selectors, unused_symbols, symbols, parent_is_unused)) return true;
},
.nesting => {
if (parent_is_unused) return true;
},
else => {},
}
}
return false;
}
/// The serialization module ported from lightningcss.
///
/// Note that we have two serialization modules, one from lightningcss and one from servo.
///
/// This is because it actually uses both implementations. This is confusing.
pub const serialize = struct {
pub fn serializeSelectorList(
list: []const parser.Selector,
comptime W: type,
dest: *Printer(W),
context: ?*const css.StyleContext,
is_relative: bool,
) PrintErr!void {
var first = true;
for (list) |*selector| {
if (!first) {
try dest.delim(',', false);
}
first = false;
try serializeSelector(selector, W, dest, context, is_relative);
}
}
pub fn serializeSelector(
selector: *const parser.Selector,
comptime W: type,
dest: *css.Printer(W),
context: ?*const css.StyleContext,
__is_relative: bool,
) PrintErr!void {
var is_relative = __is_relative;
if (comptime bun.Environment.isDebug) {
debug("Selector components:\n", .{});
for (selector.components.items) |*comp| {
debug(" {}\n", .{comp});
}
debug("Compound selector iter\n", .{});
var compound_selectors = CompoundSelectorIter{ .sel = selector };
while (compound_selectors.next()) |comp| {
for (comp) |c| {
debug(" {}, ", .{c});
}
}
debug("\n", .{});
}
// Compound selectors invert the order of their contents, so we need to
// undo that during serialization.
//
// This two-iterator strategy involves walking over the selector twice.
// We could do something more clever, but selector serialization probably
// isn't hot enough to justify it, and the stringification likely
// dominates anyway.
//
// NB: A parse-order iterator is a Rev<>, which doesn't expose as_slice(),
// which we need for |split|. So we split by combinators on a match-order
// sequence and then reverse.
var combinators = CombinatorIter{ .sel = selector };
var compound_selectors = CompoundSelectorIter{ .sel = selector };
const should_compile_nesting = dest.targets.shouldCompileSame(.nesting);
var first = true;
var combinators_exhausted = false;
while (compound_selectors.next()) |_compound_| {
bun.debugAssert(!combinators_exhausted);
var compound = _compound_;
// Skip implicit :scope in relative selectors (e.g. :has(:scope > foo) -> :has(> foo))
if (is_relative and compound.len >= 1 and compound[0] == .scope) {
if (combinators.next()) |*combinator| {
try serializeCombinator(combinator, W, dest);
}
compound = compound[1..];
is_relative = false;
}
// https://drafts.csswg.org/cssom/#serializing-selectors
if (compound.len == 0) continue;
const has_leading_nesting = first and compound[0] == .nesting;
const first_index: usize = if (has_leading_nesting) 1 else 0;
first = false;
// 1. If there is only one simple selector in the compound selectors
// which is a universal selector, append the result of
// serializing the universal selector to s.
//
// Check if `!compound{}` first--this can happen if we have
// something like `... > ::before`, because we store `>` and `::`
// both as combinators internally.
//
// If we are in this case, after we have serialized the universal
// selector, we skip Step 2 and continue with the algorithm.
const can_elide_namespace, const first_non_namespace = if (first_index >= compound.len)
.{ true, first_index }
else switch (compound[0]) {
.explicit_any_namespace, .explicit_no_namespace, .namespace => .{ false, first_index + 1 },
.default_namespace => .{ true, first_index + 1 },
else => .{ true, first_index },
};
var perform_step_2 = true;
const next_combinator = combinators.next();
if (first_non_namespace == compound.len - 1) {
// We have to be careful here, because if there is a
// pseudo element "combinator" there isn't really just
// the one simple selector. Technically this compound
// selector contains the pseudo element selector as well
// -- Combinator::PseudoElement, just like
// Combinator::SlotAssignment, don't exist in the
// spec.
if (next_combinator == .pseudo_element and compound[first_non_namespace].asCombinator() == .slot_assignment) {
// do nothing
} else if (compound[first_non_namespace] == .explicit_universal_type) {
// Iterate over everything so we serialize the namespace
// too.
const swap_nesting = has_leading_nesting and should_compile_nesting;
const slice = if (swap_nesting) brk: {
// Swap nesting and type selector (e.g. &div -> div&).
break :brk compound[@min(1, compound.len)..];
} else compound;
for (slice) |*simple| {
try serializeComponent(simple, W, dest, context);
}
if (swap_nesting) {
try serializeNesting(W, dest, context, false);
}
// Skip step 2, which is an "otherwise".
perform_step_2 = false;
} else {
// do nothing
}
}
// 2. Otherwise, for each simple selector in the compound selectors
// that is not a universal selector of which the namespace prefix
// maps to a namespace that is not the default namespace
// serialize the simple selector and append the result to s.
//
// See https://github.com/w3c/csswg-drafts/issues/1606, which is
// proposing to change this to match up with the behavior asserted
// in cssom/serialize-namespaced-type-selectors.html, which the
// following code tries to match.
if (perform_step_2) {
const iter = compound;
var i: usize = 0;
if (has_leading_nesting and
should_compile_nesting and
isTypeSelector(if (first_non_namespace < compound.len) &compound[first_non_namespace] else null))
{
// Swap nesting and type selector (e.g. &div -> div&).
// This ensures that the compiled selector is valid. e.g. (div.foo is valid, .foodiv is not).
const nesting = &iter[i];
i += 1;
const local = &iter[i];
i += 1;
try serializeComponent(local, W, dest, context);
// Also check the next item in case of namespaces.
if (first_non_namespace > first_index) {
const local2 = &iter[i];
i += 1;
try serializeComponent(local2, W, dest, context);
}
try serializeComponent(nesting, W, dest, context);
} else if (has_leading_nesting and should_compile_nesting) {
// Nesting selector may serialize differently if it is leading, due to type selectors.
i += 1;
try serializeNesting(W, dest, context, true);
}
if (i < compound.len) {
for (iter[i..]) |*simple| {
if (simple.* == .explicit_universal_type) {
// Can't have a namespace followed by a pseudo-element
// selector followed by a universal selector in the same
// compound selector, so we don't have to worry about the
// real namespace being in a different `compound`.
if (can_elide_namespace) {
continue;
}
}
try serializeComponent(simple, W, dest, context);
}
}
}
// 3. If this is not the last part of the chain of the selector
// append a single SPACE (U+0020), followed by the combinator
// ">", "+", "~", ">>", "||", as appropriate, followed by another
// single SPACE (U+0020) if the combinator was not whitespace, to
// s.
if (next_combinator) |*c| {
try serializeCombinator(c, W, dest);
} else {
combinators_exhausted = true;
}
// 4. If this is the last part of the chain of the selector and
// there is a pseudo-element, append "::" followed by the name of
// the pseudo-element, to s.
//
// (we handle this above)
}
}
pub fn serializeComponent(
component: *const parser.Component,
comptime W: type,
dest: *css.Printer(W),
context: ?*const css.StyleContext,
) PrintErr!void {
switch (component.*) {
.combinator => |c| return serializeCombinator(&c, W, dest),
.attribute_in_no_namespace => |*v| {
try dest.writeChar('[');
try css.css_values.ident.IdentFns.toCss(&v.local_name, W, dest);
try v.operator.toCss(W, dest);
if (dest.minify) {
// PERF: should we put a scratch buffer in the printer
// Serialize as both an identifier and a string and choose the shorter one.
var id = std.ArrayList(u8).init(dest.allocator);
const writer = id.writer();
css.serializer.serializeIdentifier(v.value, writer) catch return dest.addFmtError();
const s = try css.to_css.string(dest.allocator, CSSString, &v.value, css.PrinterOptions.default(), dest.import_info, dest.local_names, dest.symbols);
if (id.items.len > 0 and id.items.len < s.len) {
try dest.writeStr(id.items);
} else {
try dest.writeStr(s);
}
} else {
try css.CSSStringFns.toCss(&v.value, W, dest);
}
switch (v.case_sensitivity) {
.case_sensitive, .ascii_case_insensitive_if_in_html_element_in_html_document => {},
.ascii_case_insensitive => try dest.writeStr(" i"),
.explicit_case_sensitive => try dest.writeStr(" s"),
}
return dest.writeChar(']');
},
.is, .where, .negation, .any => {
switch (component.*) {
.where => try dest.writeStr(":where("),
.is => |selectors| {
// If there's only one simple selector, serialize it directly.
if (shouldUnwrapIs(selectors)) {
return serializeSelector(&selectors[0], W, dest, context, false);
}
const vp = dest.vendor_prefix;
if (vp.webkit or vp.moz) {
try dest.writeChar(':');
try vp.toCss(W, dest);
try dest.writeStr("any(");
} else {
try dest.writeStr(":is(");
}
},
.negation => {
try dest.writeStr(":not(");
},
.any => |v| {
const vp = dest.vendor_prefix._or(v.vendor_prefix);
if (vp.webkit or vp.moz) {
try dest.writeChar(':');
try vp.toCss(W, dest);
try dest.writeStr("any(");
} else {
try dest.writeStr(":is(");
}
},
else => unreachable,
}
try serializeSelectorList(switch (component.*) {
.where, .is, .negation => |list| list,
.any => |v| v.selectors,
else => unreachable,
}, W, dest, context, false);
return dest.writeStr(")");
},
.has => |list| {
try dest.writeStr(":has(");
try serializeSelectorList(list, W, dest, context, true);
return dest.writeStr(")");
},
.non_ts_pseudo_class => |*pseudo| {
return serializePseudoClass(pseudo, W, dest, context);
},
.pseudo_element => |*pseudo| {
return serializePseudoElement(pseudo, W, dest, context);
},
.nesting => {
return serializeNesting(W, dest, context, false);
},
.class => |class| {
try dest.writeChar('.');
return dest.writeIdentOrRef(class, dest.css_module != null);
},
.id => |id| {
try dest.writeChar('#');
return dest.writeIdentOrRef(id, dest.css_module != null);
},
.host => |selector| {
try dest.writeStr(":host");
if (selector) |*sel| {
try dest.writeChar('(');
try serializeSelector(sel, W, dest, dest.context(), false);
try dest.writeChar(')');
}
return;
},
.slotted => |*selector| {
try dest.writeStr("::slotted(");
try serializeSelector(selector, W, dest, dest.context(), false);
try dest.writeChar(')');
},
// .nth => |nth_data| {
// try nth_data.writeStart(W, dest, nth_data.isFunction());
// if (nth_data.isFunction()) {
// try nth_data.writeAffine(W, dest);
// try dest.writeChar(')');
// }
// },
else => {
try tocss_servo.toCss_Component(component, W, dest);
},
}
}
pub fn serializeCombinator(
combinator: *const parser.Combinator,
comptime W: type,
dest: *Printer(W),
) PrintErr!void {
switch (combinator.*) {
.child => try dest.delim('>', true),
.descendant => try dest.writeStr(" "),
.next_sibling => try dest.delim('+', true),
.later_sibling => try dest.delim('~', true),
.deep => try dest.writeStr(" /deep/ "),
.deep_descendant => {
try dest.whitespace();
try dest.writeStr(">>>");
try dest.whitespace();
},
.pseudo_element, .part, .slot_assignment => return,
}
}
pub fn serializePseudoClass(
pseudo_class: *const parser.PseudoClass,
comptime W: type,
dest: *Printer(W),
context: ?*const css.StyleContext,
) PrintErr!void {
switch (pseudo_class.*) {
.lang => {
try dest.writeStr(":lang(");
var first = true;
for (pseudo_class.lang.languages.items) |lang| {
if (first) {
first = false;
} else {
try dest.delim(',', false);
}
css.serializer.serializeIdentifier(lang, dest) catch return dest.addFmtError();
}
return dest.writeStr(")");
},
.dir => {
const dir = pseudo_class.dir.direction;
try dest.writeStr(":dir(");
try dir.toCss(W, dest);
return try dest.writeStr(")");
},
else => {},
}
const Helpers = struct {
pub inline fn writePrefixed(
d: *Printer(W),
prefix: css.VendorPrefix,
comptime val: []const u8,
) PrintErr!void {
try d.writeChar(':');
// If the printer has a vendor prefix override, use that.
const vp = if (!d.vendor_prefix.isEmpty())
bun.bits.@"or"(css.VendorPrefix, d.vendor_prefix, prefix).orNone()
else
prefix;
try vp.toCss(W, d);
try d.writeStr(val);
}
pub inline fn pseudo(
d: *Printer(W),
comptime key: []const u8,
comptime s: []const u8,
) PrintErr!void {
const key_snake_case = comptime key_snake_case: {
var buf: [key.len]u8 = undefined;
for (key, 0..) |c, i| {
buf[i] = if (c >= 'A' and c <= 'Z') c + 32 else if (c == '-') '_' else c;
}
const buf2 = buf;
break :key_snake_case buf2;
};
const _class = if (d.pseudo_classes) |*pseudo_classes| @field(pseudo_classes, &key_snake_case) else null;
if (_class) |class| {
try d.writeChar('.');
try d.writeIdent(class, true);
} else {
try d.writeStr(s);
}
}
};
switch (pseudo_class.*) {
// https://drafts.csswg.org/selectors-4/#useraction-pseudos
.hover => try Helpers.pseudo(dest, "hover", ":hover"),
.active => try Helpers.pseudo(dest, "active", ":active"),
.focus => try Helpers.pseudo(dest, "focus", ":focus"),
.focus_visible => try Helpers.pseudo(dest, "focus-visible", ":focus-visible"),
.focus_within => try Helpers.pseudo(dest, "focus-within", ":focus-within"),
// https://drafts.csswg.org/selectors-4/#time-pseudos
.current => try dest.writeStr(":current"),
.past => try dest.writeStr(":past"),
.future => try dest.writeStr(":future"),
// https://drafts.csswg.org/selectors-4/#resource-pseudos
.playing => try dest.writeStr(":playing"),
.paused => try dest.writeStr(":paused"),
.seeking => try dest.writeStr(":seeking"),
.buffering => try dest.writeStr(":buffering"),
.stalled => try dest.writeStr(":stalled"),
.muted => try dest.writeStr(":muted"),
.volume_locked => try dest.writeStr(":volume-locked"),
// https://fullscreen.spec.whatwg.org/#:fullscreen-pseudo-class
.fullscreen => |prefix| {
try dest.writeChar(':');
const vp = if (!dest.vendor_prefix.isEmpty())
bits.@"and"(css.VendorPrefix, dest.vendor_prefix, prefix).orNone()
else
prefix;
try vp.toCss(W, dest);
if (vp.webkit or vp.moz) {
try dest.writeStr("full-screen");
} else {
try dest.writeStr("fullscreen");
}
},
// https://drafts.csswg.org/selectors/#display-state-pseudos
.open => try dest.writeStr(":open"),
.closed => try dest.writeStr(":closed"),
.modal => try dest.writeStr(":modal"),
.picture_in_picture => try dest.writeStr(":picture-in-picture"),
// https://html.spec.whatwg.org/multipage/semantics-other.html#selector-popover-open
.popover_open => try dest.writeStr(":popover-open"),
// https://drafts.csswg.org/selectors-4/#the-defined-pseudo
.defined => try dest.writeStr(":defined"),
// https://drafts.csswg.org/selectors-4/#location
.any_link => |prefix| try Helpers.writePrefixed(dest, prefix, "any-link"),
.link => try dest.writeStr(":link"),
.local_link => try dest.writeStr(":local-link"),
.target => try dest.writeStr(":target"),
.target_within => try dest.writeStr(":target-within"),
.visited => try dest.writeStr(":visited"),
// https://drafts.csswg.org/selectors-4/#input-pseudos
.enabled => try dest.writeStr(":enabled"),
.disabled => try dest.writeStr(":disabled"),
.read_only => |prefix| try Helpers.writePrefixed(dest, prefix, "read-only"),
.read_write => |prefix| try Helpers.writePrefixed(dest, prefix, "read-write"),
.placeholder_shown => |prefix| try Helpers.writePrefixed(dest, prefix, "placeholder-shown"),
.default => try dest.writeStr(":default"),
.checked => try dest.writeStr(":checked"),
.indeterminate => try dest.writeStr(":indeterminate"),
.blank => try dest.writeStr(":blank"),
.valid => try dest.writeStr(":valid"),
.invalid => try dest.writeStr(":invalid"),
.in_range => try dest.writeStr(":in-range"),
.out_of_range => try dest.writeStr(":out-of-range"),
.required => try dest.writeStr(":required"),
.optional => try dest.writeStr(":optional"),
.user_valid => try dest.writeStr(":user-valid"),
.user_invalid => try dest.writeStr(":user-invalid"),
// https://html.spec.whatwg.org/multipage/semantics-other.html#selector-autofill
.autofill => |prefix| try Helpers.writePrefixed(dest, prefix, "autofill"),
.local => |selector| try serializeSelector(selector.selector, W, dest, context, false),
.global => |selector| {
const css_module = if (dest.css_module) |module| css_module: {
dest.css_module = null;
break :css_module module;
} else null;
try serializeSelector(selector.selector, W, dest, context, false);
dest.css_module = css_module;
},
// https://webkit.org/blog/363/styling-scrollbars/
.webkit_scrollbar => |s| {
try dest.writeStr(switch (s) {
.horizontal => ":horizontal",
.vertical => ":vertical",
.decrement => ":decrement",
.increment => ":increment",
.start => ":start",
.end => ":end",
.double_button => ":double-button",
.single_button => ":single-button",
.no_button => ":no-button",
.corner_present => ":corner-present",
.window_inactive => ":window-inactive",
});
},
.lang => unreachable,
.dir => unreachable,
.custom => |name| {
try dest.writeChar(':');
return dest.writeStr(name.name);
},
.custom_function => |v| {
try dest.writeChar(':');
try dest.writeStr(v.name);
try dest.writeChar('(');
try v.arguments.toCssRaw(W, dest);
try dest.writeChar(')');
},
}
}
pub fn serializePseudoElement(
pseudo_element: *const parser.PseudoElement,
comptime W: type,
dest: *Printer(W),
context: ?*const css.StyleContext,
) PrintErr!void {
const Helpers = struct {
pub fn writePrefix(d: *Printer(W), prefix: css.VendorPrefix) PrintErr!css.VendorPrefix {
try d.writeStr("::");
// If the printer has a vendor prefix override, use that.
const vp = if (!d.vendor_prefix.isEmpty())
bits.@"and"(css.VendorPrefix, d.vendor_prefix, prefix).orNone()
else
prefix;
try vp.toCss(W, d);
debug("VENDOR PREFIX {d} OVERRIDE {d}", .{ vp.asBits(), d.vendor_prefix.asBits() });
return vp;
}
pub fn writePrefixed(d: *Printer(W), prefix: css.VendorPrefix, comptime val: []const u8) PrintErr!void {
_ = try writePrefix(d, prefix);
try d.writeStr(val);
}
};
// switch (pseudo_element.*) {
// // CSS2 pseudo elements support a single colon syntax in addition
// // to the more correct double colon for other pseudo elements.
// // We use that here because it's supported everywhere and is shorter.
// .after => try dest.writeStr(":after"),
// .before => try dest.writeStr(":before"),
// .marker => try dest.writeStr(":first-letter"),
// .selection => |prefix| Helpers.writePrefixed(dest, prefix, "selection"),
// .cue => dest.writeStr("::cue"),
// .cue_region => dest.writeStr("::cue-region"),
// .cue_function => |v| {
// dest.writeStr("::cue(");
// try serializeSelector(v.selector, W, dest, context, false);
// try dest.writeChar(')');
// },
// }
switch (pseudo_element.*) {
// CSS2 pseudo elements support a single colon syntax in addition
// to the more correct double colon for other pseudo elements.
// We use that here because it's supported everywhere and is shorter.
.after => try dest.writeStr(":after"),
.before => try dest.writeStr(":before"),
.first_line => try dest.writeStr(":first-line"),
.first_letter => try dest.writeStr(":first-letter"),
.marker => try dest.writeStr("::marker"),
.selection => |prefix| try Helpers.writePrefixed(dest, prefix, "selection"),
.cue => try dest.writeStr("::cue"),
.cue_region => try dest.writeStr("::cue-region"),
.cue_function => |v| {
try dest.writeStr("::cue(");
try serializeSelector(v.selector, W, dest, context, false);
try dest.writeChar(')');
},
.cue_region_function => |v| {
try dest.writeStr("::cue-region(");
try serializeSelector(v.selector, W, dest, context, false);
try dest.writeChar(')');
},
.placeholder => |prefix| {
const vp = try Helpers.writePrefix(dest, prefix);
if (vp.webkit or vp.ms) {
try dest.writeStr("input-placeholder");
} else {
try dest.writeStr("placeholder");
}
},
.backdrop => |prefix| try Helpers.writePrefixed(dest, prefix, "backdrop"),
.file_selector_button => |prefix| {
const vp = try Helpers.writePrefix(dest, prefix);
if (vp.webkit) {
try dest.writeStr("file-upload-button");
} else if (vp.ms) {
try dest.writeStr("browse");
} else {
try dest.writeStr("file-selector-button");
}
},
.webkit_scrollbar => |s| {
try dest.writeStr(switch (s) {
.scrollbar => "::-webkit-scrollbar",
.button => "::-webkit-scrollbar-button",
.track => "::-webkit-scrollbar-track",
.track_piece => "::-webkit-scrollbar-track-piece",
.thumb => "::-webkit-scrollbar-thumb",
.corner => "::-webkit-scrollbar-corner",
.resizer => "::-webkit-resizer",
});
},
.view_transition => try dest.writeStr("::view-transition"),
.view_transition_group => |v| {
try dest.writeStr("::view-transition-group(");
try v.part_name.toCss(W, dest);
try dest.writeChar(')');
},
.view_transition_image_pair => |v| {
try dest.writeStr("::view-transition-image-pair(");
try v.part_name.toCss(W, dest);
try dest.writeChar(')');
},
.view_transition_old => |v| {
try dest.writeStr("::view-transition-old(");
try v.part_name.toCss(W, dest);
try dest.writeChar(')');
},
.view_transition_new => |v| {
try dest.writeStr("::view-transition-new(");
try v.part_name.toCss(W, dest);
try dest.writeChar(')');
},
.custom => |val| {
try dest.writeStr("::");
return dest.writeStr(val.name);
},
.custom_function => |v| {
const name = v.name;
try dest.writeStr("::");
try dest.writeStr(name);
try dest.writeChar('(');
try v.arguments.toCssRaw(W, dest);
try dest.writeChar(')');
},
}
}
pub fn serializeNesting(
comptime W: type,
dest: *Printer(W),
context: ?*const css.StyleContext,
first: bool,
) PrintErr!void {
if (context) |ctx| {
// If there's only one simple selector, just serialize it directly.
// Otherwise, use an :is() pseudo class.
// Type selectors are only allowed at the start of a compound selector,
// so use :is() if that is not the case.
if (ctx.selectors.v.len() == 1 and
(first or (!hasTypeSelector(ctx.selectors.v.at(0)) and
isSimple(ctx.selectors.v.at(0)))))
{
try serializeSelector(ctx.selectors.v.at(0), W, dest, ctx.parent, false);
} else {
try dest.writeStr(":is(");
try serializeSelectorList(ctx.selectors.v.slice(), W, dest, ctx.parent, false);
try dest.writeChar(')');
}
} else {
// If there is no context, we are at the root if nesting is supported. This is equivalent to :scope.
// Otherwise, if nesting is supported, serialize the nesting selector directly.
if (dest.targets.shouldCompileSame(.nesting)) {
try dest.writeStr(":scope");
} else {
try dest.writeChar('&');
}
}
}
};
pub const tocss_servo = struct {
pub fn toCss_SelectorList(
selectors: []const parser.Selector,
comptime W: type,
dest: *css.Printer(W),
) PrintErr!void {
if (selectors.len == 0) {
return;
}
try tocss_servo.toCss_Selector(&selectors[0], W, dest);
if (selectors.len > 1) {
for (selectors[1..]) |*selector| {
try dest.writeStr(", ");
try tocss_servo.toCss_Selector(selector, W, dest);
}
}
}
pub fn toCss_Selector(
selector: *const parser.Selector,
comptime W: type,
dest: *css.Printer(W),
) PrintErr!void {
// Compound selectors invert the order of their contents, so we need to
// undo that during serialization.
//
// This two-iterator strategy involves walking over the selector twice.
// We could do something more clever, but selector serialization probably
// isn't hot enough to justify it, and the stringification likely
// dominates anyway.
//
// NB: A parse-order iterator is a Rev<>, which doesn't expose as_slice(),
// which we need for |split|. So we split by combinators on a match-order
// sequence and then reverse.
var combinators = CombinatorIter{ .sel = selector };
var compound_selectors = CompoundSelectorIter{ .sel = selector };
var combinators_exhausted = false;
while (compound_selectors.next()) |compound| {
bun.debugAssert(!combinators_exhausted);
// https://drafts.csswg.org/cssom/#serializing-selectors
if (compound.len == 0) continue;
// 1. If there is only one simple selector in the compound selectors
// which is a universal selector, append the result of
// serializing the universal selector to s.
//
// Check if `!compound{}` first--this can happen if we have
// something like `... > ::before`, because we store `>` and `::`
// both as combinators internally.
//
// If we are in this case, after we have serialized the universal
// selector, we skip Step 2 and continue with the algorithm.
const can_elide_namespace, const first_non_namespace: usize = if (0 >= compound.len)
.{ true, 0 }
else switch (compound[0]) {
.explicit_any_namespace, .explicit_no_namespace, .namespace => .{ false, 1 },
.default_namespace => .{ true, 1 },
else => .{ true, 0 },
};
var perform_step_2 = true;
const next_combinator = combinators.next();
if (first_non_namespace == compound.len - 1) {
// We have to be careful here, because if there is a
// pseudo element "combinator" there isn't really just
// the one simple selector. Technically this compound
// selector contains the pseudo element selector as well
// -- Combinator::PseudoElement, just like
// Combinator::SlotAssignment, don't exist in the
// spec.
if (next_combinator == .pseudo_element and compound[first_non_namespace].asCombinator() == .slot_assignment) {
// do nothing
} else if (compound[first_non_namespace] == .explicit_universal_type) {
// Iterate over everything so we serialize the namespace
// too.
for (compound) |*simple| {
try tocss_servo.toCss_Component(simple, W, dest);
}
// Skip step 2, which is an "otherwise".
perform_step_2 = false;
} else {
// do nothing
}
}
// 2. Otherwise, for each simple selector in the compound selectors
// that is not a universal selector of which the namespace prefix
// maps to a namespace that is not the default namespace
// serialize the simple selector and append the result to s.
//
// See https://github.com/w3c/csswg-drafts/issues/1606, which is
// proposing to change this to match up with the behavior asserted
// in cssom/serialize-namespaced-type-selectors.html, which the
// following code tries to match.
if (perform_step_2) {
for (compound) |*simple| {
if (simple.* == .explicit_universal_type) {
// Can't have a namespace followed by a pseudo-element
// selector followed by a universal selector in the same
// compound selector, so we don't have to worry about the
// real namespace being in a different `compound`.
if (can_elide_namespace) {
continue;
}
}
try tocss_servo.toCss_Component(simple, W, dest);
}
}
// 3. If this is not the last part of the chain of the selector
// append a single SPACE (U+0020), followed by the combinator
// ">", "+", "~", ">>", "||", as appropriate, followed by another
// single SPACE (U+0020) if the combinator was not whitespace, to
// s.
if (next_combinator) |c| {
try toCss_Combinator(&c, W, dest);
} else {
combinators_exhausted = true;
}
// 4. If this is the last part of the chain of the selector and
// there is a pseudo-element, append "::" followed by the name of
// the pseudo-element, to s.
//
// (we handle this above)
}
}
pub fn toCss_Component(
component: *const parser.Component,
comptime W: type,
dest: *Printer(W),
) PrintErr!void {
switch (component.*) {
.combinator => |*c| try toCss_Combinator(c, W, dest),
.slotted => |*selector| {
try dest.writeStr("::slotted(");
try tocss_servo.toCss_Selector(selector, W, dest);
try dest.writeChar(')');
},
.part => |part_names| {
try dest.writeStr("::part(");
for (part_names, 0..) |name, i| {
if (i != 0) {
try dest.writeChar(' ');
}
try css.IdentFns.toCss(&name, W, dest);
}
try dest.writeChar(')');
},
.pseudo_element => |*p| {
try p.toCss(W, dest);
},
.id => |s| {
try dest.writeChar('#');
const str = dest.lookupIdentOrRef(s);
try dest.writeStr(str);
},
.class => |s| {
try dest.writeChar('.');
const str = dest.lookupIdentOrRef(s);
try dest.writeStr(str);
},
.local_name => |local_name| {
try local_name.toCss(W, dest);
},
.explicit_universal_type => {
try dest.writeChar('*');
},
.default_namespace => return,
.explicit_no_namespace => {
try dest.writeChar('|');
},
.explicit_any_namespace => {
try dest.writeStr("*|");
},
.namespace => |ns| {
try css.IdentFns.toCss(&ns.prefix, W, dest);
try dest.writeChar('|');
},
.attribute_in_no_namespace_exists => |v| {
try dest.writeChar('[');
try css.IdentFns.toCss(&v.local_name, W, dest);
try dest.writeChar(']');
},
.attribute_in_no_namespace => |v| {
try dest.writeChar('[');
try css.IdentFns.toCss(&v.local_name, W, dest);
try v.operator.toCss(W, dest);
try css.CSSStringFns.toCss(&v.value, W, dest);
switch (v.case_sensitivity) {
.case_sensitive, .ascii_case_insensitive_if_in_html_element_in_html_document => {},
.ascii_case_insensitive => try dest.writeStr(" i"),
.explicit_case_sensitive => try dest.writeStr(" s"),
}
try dest.writeChar(']');
},
.attribute_other => |attr_selector| {
try attr_selector.toCss(W, dest);
},
// Pseudo-classes
.root => {
try dest.writeStr(":root");
},
.empty => {
try dest.writeStr(":empty");
},
.scope => {
try dest.writeStr(":scope");
},
.host => |selector| {
try dest.writeStr(":host");
if (selector) |*sel| {
try dest.writeChar('(');
try tocss_servo.toCss_Selector(sel, W, dest);
try dest.writeChar(')');
}
},
.nth => |nth_data| {
try nth_data.writeStart(W, dest, nth_data.isFunction());
if (nth_data.isFunction()) {
try nth_data.writeAffine(W, dest);
try dest.writeChar(')');
}
},
.nth_of => |nth_of_data| {
const nth_data = nth_of_data.nthData();
try nth_data.writeStart(W, dest, true);
// A selector must be a function to hold An+B notation
bun.debugAssert(nth_data.is_function);
try nth_data.writeAffine(W, dest);
// Only :nth-child or :nth-last-child can be of a selector list
bun.debugAssert(nth_data.ty == .child or nth_data.ty == .last_child);
// The selector list should not be empty
bun.debugAssert(nth_of_data.selectors.len != 0);
try dest.writeStr(" of ");
try tocss_servo.toCss_SelectorList(nth_of_data.selectors, W, dest);
try dest.writeChar(')');
},
.is, .where, .negation, .has, .any => {
switch (component.*) {
.where => try dest.writeStr(":where("),
.is => try dest.writeStr(":is("),
.negation => try dest.writeStr(":not("),
.has => try dest.writeStr(":has("),
.any => |v| {
try dest.writeChar(':');
try v.vendor_prefix.toCss(W, dest);
try dest.writeStr("any(");
},
else => unreachable,
}
try tocss_servo.toCss_SelectorList(
switch (component.*) {
.where, .is, .negation, .has => |list| list,
.any => |v| v.selectors,
else => unreachable,
},
W,
dest,
);
try dest.writeStr(")");
},
.non_ts_pseudo_class => |*pseudo| {
try pseudo.toCss(W, dest);
},
.nesting => try dest.writeChar('&'),
}
}
pub fn toCss_Combinator(
combinator: *const parser.Combinator,
comptime W: type,
dest: *Printer(W),
) PrintErr!void {
switch (combinator.*) {
.child => try dest.writeStr(" > "),
.descendant => try dest.writeStr(" "),
.next_sibling => try dest.writeStr(" + "),
.later_sibling => try dest.writeStr(" ~ "),
.deep => try dest.writeStr(" /deep/ "),
.deep_descendant => {
try dest.writeStr(" >>> ");
},
.pseudo_element, .part, .slot_assignment => return,
}
}
pub fn toCss_PseudoElement(
pseudo_element: *const parser.PseudoElement,
comptime W: type,
dest: *Printer(W),
) PrintErr!void {
switch (pseudo_element.*) {
.before => try dest.writeStr("::before"),
.after => try dest.writeStr("::after"),
}
}
};
pub fn shouldUnwrapIs(selectors: []const parser.Selector) bool {
if (selectors.len == 1) {
const first = selectors[0];
if (!hasTypeSelector(&first) and isSimple(&first)) return true;
}
return false;
}
fn hasTypeSelector(selector: *const parser.Selector) bool {
var iter = selector.iterRawMatchOrder();
const first = iter.next();
if (isNamespace(if (first) |*f| f else null)) return isTypeSelector(if (iter.next()) |*n| n else null);
return isTypeSelector(if (first) |*f| f else null);
}
fn isNamespace(component: ?*const parser.Component) bool {
if (component) |c| return switch (c.*) {
.explicit_any_namespace, .explicit_no_namespace, .namespace, .default_namespace => true,
else => false,
};
return false;
}
fn isTypeSelector(component: ?*const parser.Component) bool {
if (component) |c| return switch (c.*) {
.local_name, .explicit_universal_type => true,
else => false,
};
return false;
}
fn isSimple(selector: *const parser.Selector) bool {
var iter = selector.iterRawParseOrderFrom(0);
const any_is_combinator = any_is_combinator: {
while (iter.next()) |component| {
if (component.isCombinator()) break :any_is_combinator true;
}
break :any_is_combinator false;
};
return !any_is_combinator;
}
const CombinatorIter = struct {
sel: *const parser.Selector,
i: usize = 0,
/// Original source has this iterator defined like so:
/// ```rs
/// selector
/// .iter_raw_match_order() // just returns an iterator
/// .rev() // reverses the iterator
/// .filter_map(|x| x.as_combinator()) // returns only entries which are combinators
/// ```
pub fn next(this: *@This()) ?parser.Combinator {
while (this.i < this.sel.components.items.len) {
defer this.i += 1;
const combinator = this.sel.components.items[this.sel.components.items.len - 1 - this.i].asCombinator() orelse continue;
return combinator;
}
return null;
}
};
const CompoundSelectorIter = struct {
sel: *const parser.Selector,
i: usize = 0,
/// This iterator is basically like doing `selector.components.splitByCombinator()`.
///
/// For example:
/// ```css
/// div > p.class
/// ```
///
/// The iterator would return:
/// ```
/// First slice:
/// .{
/// .{ .local_name = "div" }
/// }
///
/// Second slice:
/// .{
/// .{ .local_name = "p" },
/// .{ .class = "class" }
/// }
/// ```
///
/// BUT, the selectors are stored in reverse order, so this code needs to split the components backwards.
///
/// Original source has this iterator defined like so:
/// ```rs
/// selector
/// .iter_raw_match_order()
/// .as_slice()
/// .split(|x| x.is_combinator()) // splits the slice into subslices by elements that match over the predicate
/// .rev() // reverse
/// ```
pub inline fn next(this: *@This()) ?[]const parser.Component {
// Since we iterating backwards, we convert all indices into "backwards form" by doing `this.sel.components.items.len - 1 - i`
while (this.i < this.sel.components.items.len) {
const next_index: ?usize = next_index: {
for (this.i..this.sel.components.items.len) |j| {
if (this.sel.components.items[this.sel.components.items.len - 1 - j].isCombinator()) break :next_index j;
}
break :next_index null;
};
if (next_index) |combinator_index| {
const start = if (combinator_index == 0) 0 else combinator_index - 1;
const end = this.i;
const slice = this.sel.components.items[this.sel.components.items.len - 1 - start .. this.sel.components.items.len - end];
this.i = combinator_index + 1;
return slice;
}
const slice = this.sel.components.items[0 .. this.sel.components.items.len - 1 - this.i + 1];
this.i = this.sel.components.items.len;
return slice;
}
return null;
}
};
const bun = @import("bun");
const bits = bun.bits;
const std = @import("std");
const ArrayList = std.ArrayListUnmanaged;
const Allocator = std.mem.Allocator;