pub(crate) mod comments;
pub(crate) mod emit;
pub(crate) mod env;
pub(crate) mod eval;
pub(crate) mod extend;
pub(crate) mod file_fn;
pub(crate) mod guard;
pub(crate) mod imports;
pub(crate) mod prelude;
pub(crate) mod selector;
pub(crate) mod value;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use oxc_css_parser::ast::*;
use oxc_css_parser::pos::Span;
use oxc_css_parser::{Allocator, ParserBuilder, Syntax};
use crate::doc::{self, Body, DeclItem, Item, Style};
use crate::merge::Merged;
use crate::{CompileError, Options, Output};
use comments::collect_definition_spans;
use emit::{ChildItem, Deferred, Piece, Sink};
pub(crate) use env::RuleSet;
use env::{Defs, Env, Inject, Roots, Vars};
use eval::collect_mixins;
use extend::{Need, Sel};
use imports::expand_imports;
use prelude::{has_plugin, is_charset, is_import};
use value::VarRef;
pub(super) fn strip_embedded_important(text: &str) -> Option<String> {
let trimmed = text.trim_end();
let cut = trimmed.len().checked_sub("!important".len())?;
if !trimmed[cut..].eq_ignore_ascii_case("!important") {
return None;
}
Some(trimmed[..cut].trim_end().to_string())
}
pub(crate) struct Compiler<'a> {
env: Env<'a>,
merged: &'a Merged,
src: &'a str,
multi_source: bool,
file_path: PathBuf,
deps: Vec<String>,
seen: Vec<PathBuf>,
mixins: Defs<'a>,
path: Vec<String>,
path_linkable: Vec<bool>,
active: Vec<(*const SimpleBlock<'a>, String)>,
rule_bodies: Vec<*const SimpleBlock<'a>>,
force_important: bool,
comments: Vec<(usize, usize)>,
comment_done: Vec<bool>,
comment_idx: usize,
options: &'a Options,
extends: extend::Engine,
scope: usize,
deferred: Vec<Vec<Deferred<'a>>>,
media: Vec<Option<(String, usize)>>,
charset: bool,
def_spans: Vec<Span>,
reparsed: std::collections::HashSet<usize>,
hidden: bool,
ref_ranges: Vec<(Vec<usize>, usize, usize)>,
}
pub(crate) fn compile(
path: &Path,
source: &str,
opts: &Options,
) -> Result<Output, Box<CompileError>> {
let mut seen = vec![path.canonicalize().unwrap_or_else(|_| path.to_path_buf())];
let mut deps = Vec::new();
let mut merged = Merged::new();
let part = expand_imports(
path,
source,
opts,
&mut seen,
&mut Vec::new(),
&mut deps,
false,
&mut merged,
);
merged.append_part(part);
let source = merged.text.as_str();
let alloc = Allocator::default();
let mut parser = ParserBuilder::new(&alloc, source)
.syntax(Syntax::Less)
.comments()
.build();
let ast = parser.parse::<Stylesheet>().map_err(|e| {
let (file, local) = merged
.locate_exact(e.span.start)
.unwrap_or((0, e.span.start));
let (path, line, column) = match merged.position_in(file, local) {
Some((path, line, column)) => (Some(path), Some(line), Some(column)),
None => (None, None, None),
};
Box::new(CompileError {
message: format!("less parse failed: {}", e.kind),
path,
line,
column,
})
})?;
let comments = parser
.comments()
.iter()
.filter(|c| matches!(c.kind, oxc_css_parser::token::CommentKind::Block))
.map(|c| (c.span.start, c.span.end))
.collect::<Vec<_>>();
let mut compiler = Compiler::new(path, &merged, opts);
compiler.env.plugins = has_plugin(&ast.statements);
compiler.comments = comments;
compiler.comment_done = vec![false; compiler.comments.len()];
compiler.seen = seen;
compiler.deps = deps;
let mut frames: Vec<Vars<'_>> = Vec::new();
let mut conds: Vec<(&LessConditions<'_>, Inject<'_>)> = Vec::new();
collect_mixins(
&ast.statements,
source,
&mut frames,
&mut Vec::new(),
&mut Vec::new(),
&mut conds,
&mut compiler.mixins,
);
collect_definition_spans(&ast.statements, &mut compiler.def_spans);
let mut root_layer = Layer::default();
compiler.emit_block(
&ast.statements,
Head {
sels: &[],
spans: &[],
extends: Vec::new(),
root: true,
},
0,
false,
Span {
start: 0,
end: source.len(),
},
&mut root_layer,
)?;
compiler.flush_comments(source.len(), &mut root_layer);
debug_assert!(
root_layer.decls.is_empty(),
"the root layer produces no bare declarations"
);
compiler.finish(root_layer.items)
}
impl<'a> Compiler<'a> {
pub(super) fn new(path: &Path, merged: &'a Merged, opts: &'a Options) -> Self {
let multi_source = merged.files.len() > 1;
let mut root = HashMap::new();
for (k, v) in &opts.less.global_vars {
root.insert(k.clone(), VarRef::Text(v.clone()));
}
let mut env = Env::new(root);
env.roots = Some(Rc::new(Roots {
dir: path.parent().unwrap_or(Path::new(".")).to_path_buf(),
paths: opts.paths.clone(),
}));
Self {
env,
merged,
src: merged.text.as_str(),
multi_source,
file_path: path.to_path_buf(),
deps: Vec::new(),
seen: vec![path.to_path_buf()],
mixins: Defs::default(),
path: Vec::new(),
path_linkable: Vec::new(),
active: Vec::new(),
rule_bodies: Vec::new(),
force_important: false,
comments: Vec::new(),
comment_done: Vec::new(),
comment_idx: 0,
options: opts,
extends: extend::Engine::new(),
scope: 0,
deferred: Vec::new(),
media: Vec::new(),
charset: false,
def_spans: Vec::new(),
reparsed: std::collections::HashSet::new(),
hidden: false,
ref_ranges: Vec::new(),
}
}
pub(super) fn slice(&self, span: Span) -> &str {
self.src.get(span.start..span.end).unwrap_or("")
}
pub(super) fn finish(mut self, mut items: Vec<Item>) -> Result<Output, Box<CompileError>> {
self.extends.run();
rewrite(&mut items, &self.extends);
prune(&mut items, &mut Vec::new(), &mut self.ref_ranges);
drop_placeholders(&mut items);
if let Some((message, span)) = self.env.misses.borrow().first().cloned() {
let (path, line, column) = match self
.merged
.locate_exact(span.start)
.and_then(|(file, local)| self.merged.position_in(file, local))
{
Some((path, line, column)) => (Some(path), Some(line), Some(column)),
None => (None, None, None),
};
return Err(Box::new(CompileError {
message,
path,
line,
column,
}));
}
let entry = self.file_path.to_string_lossy();
let style = Style {
sources_content: self.multi_source,
..Style::LESS
};
let (css, map) = doc::print(&items, &style, &entry, self.merged, self.options);
Ok(Output {
css,
map,
deps: self.deps,
})
}
pub(super) fn emit_block(
&mut self,
stmts: &'a [Statement<'a>],
head: Head<'_>,
level: usize,
bare: bool,
block_span: Span,
layer: &mut Layer,
) -> Result<(), Box<CompileError>> {
let saved_hidden = self.hidden;
self.env.push();
let wrapped = !head.sels.is_empty() || bare;
let mut pieces: Vec<Piece<'a>> = Vec::new();
let mut children: Vec<ChildItem<'a>> = Vec::new();
let mut needs: Vec<Need> = Vec::new();
let mut sink = Sink {
pieces: &mut pieces,
children: &mut children,
inject: None,
needs: &mut needs,
};
self.collect_block(stmts, block_span, wrapped, &mut sink);
let rule = self.register_rule(&head, &needs);
let pieces = self.merge_pieces(pieces);
self.env.set_props(self.collect_props(&pieces));
self.mark_reparsed_props(&pieces);
let pieces = self.dedupe_pieces(pieces);
if !pieces.is_empty() && head.sels.is_empty() && bare {
for piece in &pieces {
if self.hidden && matches!(piece, Piece::Comment(_)) {
continue;
}
let item = self.piece_item(piece);
layer.decls.push(item);
}
} else if !pieces.is_empty() && !head.sels.is_empty() {
let selector = head
.sels
.iter()
.enumerate()
.map(|(i, sel)| {
let mut text = extend::render(sel);
if i + 1 < head.sels.len() {
text.push(',');
}
let span = head
.spans
.get(i)
.copied()
.unwrap_or(Span { start: 0, end: 0 });
(text, span)
})
.collect();
let body = self.piece_body(&pieces);
layer.items.push(Item::Rule(crate::doc::Rule {
selector,
body,
group_end: false,
extend_id: rule,
placeholder: false,
}));
}
if head.root {
for child in &children {
if let Child::AtRule(at) = child.child
&& is_charset(at)
{
self.emit_child_at(at, child, level, head.sels, head.spans, layer)?;
}
}
let front_limit = children
.iter()
.find_map(|c| match c.child {
Child::AtRule(at) if is_import(at) || is_charset(at) => None,
_ => Some(c.at),
})
.unwrap_or(usize::MAX);
self.flush_comments(front_limit, layer);
for child in &children {
if let Child::AtRule(at) = child.child
&& self.hoists(at, &child.inject)
{
self.emit_child_at(at, child, level, head.sels, head.spans, layer)?;
}
}
}
for child in &children {
if head.root
&& let Child::AtRule(at) = child.child
&& (is_charset(at) || self.hoists(at, &child.inject))
{
continue;
}
self.flush_comments(child.at, layer);
match child.child {
Child::Rule(rule) => self.emit_child_rule(
&rule.selector,
&rule.block,
child.inject.clone(),
child.important,
head.sels,
head.spans,
level,
layer,
)?,
Child::CondRule(rule) => self.emit_child_rule(
&rule.selector,
&rule.block,
child.inject.clone(),
child.important,
head.sels,
head.spans,
level,
layer,
)?,
Child::AtRule(at) => {
self.emit_child_at(at, child, level, head.sels, head.spans, layer)?
}
Child::Ref(block) => {
self.emit_reference(block, head.sels, head.spans, level, layer)?
}
Child::Keyframe(k) => self.emit_keyframe(k, level, layer)?,
Child::Other(f) => self.emit_other(f, layer),
}
}
self.env.pop();
self.hidden = saved_hidden;
Ok(())
}
pub(super) fn emit_reference(
&mut self,
block: &'a SimpleBlock<'a>,
parents: &[Sel],
parent_spans: &[Span],
level: usize,
layer: &mut Layer,
) -> Result<(), Box<CompileError>> {
let start = layer.items.len();
let saved = self.hidden;
self.hidden = true;
self.own_comments(block.span, &block.statements);
let result = self.emit_block(
&block.statements,
Head {
sels: parents,
spans: parent_spans,
extends: vec![Vec::new(); parents.len()],
root: false,
},
level,
false,
block.span,
layer,
);
self.hidden = saved;
self.ref_ranges
.push((layer.path.clone(), start, layer.items.len()));
result
}
pub(super) fn emit_other(&mut self, f: &'a Function<'a>, layer: &mut Layer) {
let text = value::eval_function_stmt(f, self.src, &self.env, 0);
layer.items.push(Item::Raw(crate::doc::Raw {
text,
marks: vec![(0, f.span)],
}));
}
pub(super) fn interpolate(&self, text: &str) -> String {
value::interpolate(text, self.src, &self.env, 0, None)
}
pub(super) fn interpolate_at(&self, text: &str, span: Span) -> String {
value::interpolate(text, self.src, &self.env, 0, Some(span))
}
pub(super) fn prop_name(&self, d: &Declaration) -> String {
let name = self.interpolate(self.slice(*d.name.span()));
self.prop_name_with(d, name)
}
pub(super) fn prop_name_with(&self, d: &Declaration, name: String) -> String {
match (d.name_prefix, d.name_suffix) {
(None, None) => name,
(prefix, suffix) => format!(
"{}{name}{}",
prefix.map(String::from).unwrap_or_default(),
suffix.map(String::from).unwrap_or_default()
),
}
}
}
pub(super) struct Head<'s> {
sels: &'s [Sel],
spans: &'s [Span],
extends: Vec<Vec<Need>>,
root: bool,
}
#[derive(Default)]
pub(super) struct Layer {
path: Vec<usize>,
items: Vec<Item>,
decls: Vec<DeclItem>,
}
impl Layer {
fn child(&self, index: usize) -> Layer {
let mut path = self.path.clone();
path.push(index);
Layer {
path,
items: Vec::new(),
decls: Vec::new(),
}
}
fn into_body(self, level: usize) -> Body {
if self.items.is_empty() {
return Body::Decls(self.decls);
}
if self.decls.is_empty() {
return Body::Rules(self.items);
}
let pad = format!("\n{}", " ".repeat(level));
let text = self
.decls
.iter()
.map(decl_text)
.collect::<Vec<_>>()
.join(&pad);
let mut items = self.items;
items.insert(
0,
Item::Raw(crate::doc::Raw {
text,
marks: Vec::new(),
}),
);
Body::Rules(items)
}
}
enum Child<'a> {
Rule(&'a QualifiedRule<'a>),
CondRule(&'a LessConditionalQualifiedRule<'a>),
AtRule(&'a AtRule<'a>),
Keyframe(&'a KeyframeBlock<'a>),
Other(&'a Function<'a>),
Ref(&'a SimpleBlock<'a>),
}
fn rewrite(items: &mut [Item], engine: &extend::Engine) {
for item in items.iter_mut() {
match item {
Item::Rule(rule) => {
let Some(id) = rule.extend_id else { continue };
let sels = engine.visible_selectors(id);
if sels.is_empty() {
rule.placeholder = true;
continue;
}
let last = sels.len() - 1;
rule.selector = sels
.iter()
.enumerate()
.map(|(i, (sel, span))| {
let mut text = extend::render(sel);
if i < last {
text.push(',');
}
(text, *span)
})
.collect();
}
Item::AtRule(at) => {
if let Some(Body::Rules(nested)) = at.body.as_mut() {
rewrite(nested, engine);
}
}
Item::Keyframe(_) | Item::Comment(_) | Item::Raw(_) => {}
}
}
}
fn drop_placeholders(items: &mut Vec<Item>) {
items.retain(|item| !matches!(item, Item::Rule(rule) if rule.placeholder));
for item in items.iter_mut() {
if let Item::AtRule(at) = item
&& let Some(Body::Rules(nested)) = at.body.as_mut()
{
drop_placeholders(nested);
}
}
}
fn prune(
items: &mut Vec<Item>,
path: &mut Vec<usize>,
ranges: &mut Vec<(Vec<usize>, usize, usize)>,
) {
for (i, item) in items.iter_mut().enumerate() {
if let Item::AtRule(at) = item
&& let Some(Body::Rules(nested)) = at.body.as_mut()
{
path.push(i);
prune(nested, path, ranges);
path.pop();
}
}
let mut mine: Vec<(usize, usize)> = ranges
.iter()
.filter(|(p, _, _)| p.as_slice() == path.as_slice())
.map(|&(_, start, end)| (start, end))
.collect();
mine.sort_unstable_by_key(|(start, _)| std::cmp::Reverse(*start));
for (start, end) in mine {
if start < end && end <= items.len() && !items[start..end].iter().any(has_visible_rule) {
items.drain(start..end);
}
}
}
fn has_visible_rule(item: &Item) -> bool {
match item {
Item::Rule(rule) => !rule.placeholder,
Item::Keyframe(_) => true,
Item::AtRule(at) => match at.body.as_ref() {
Some(Body::Rules(nested)) => nested.iter().any(has_visible_rule),
Some(Body::Decls(_)) | None => false,
},
Item::Comment(_) | Item::Raw(_) => false,
}
}
fn decl_text(item: &DeclItem) -> String {
match item {
DeclItem::Decl(decl) => match decl.custom {
true => format!("{}:{};", decl.name, decl.value),
false => format!("{}: {};", decl.name, decl.value),
},
DeclItem::Comment(comment) => format!("/*{}*/", comment.text),
DeclItem::AtRule(at) => match at.prelude.is_empty() {
true => format!("@{};", at.name),
false => format!("@{} {};", at.name, at.prelude),
},
}
}