use crate::ast::SrcLines;
use crate::eval::{OutItem, OutNode};
use crate::sourcemap::SmCollector;
use crate::OutputStyle;
pub(crate) fn emit(nodes: &[OutNode], style: OutputStyle, charset: bool) -> String {
emit_inner(nodes, style, &mut None, charset).0
}
pub(crate) fn emit_with_map(
nodes: &[OutNode],
style: OutputStyle,
charset: bool,
) -> (String, usize, SmCollector) {
let mut collector = Some(SmCollector::new(matches!(style, OutputStyle::Compressed)));
let (css, body_off) = emit_inner(nodes, style, &mut collector, charset);
(css, body_off, collector.expect("collector present"))
}
fn emit_inner(
nodes: &[OutNode],
style: OutputStyle,
collector: &mut Option<SmCollector>,
charset: bool,
) -> (String, usize) {
let mut body = match style {
OutputStyle::Expanded => emit_expanded(nodes, collector),
OutputStyle::Compressed => emit_compressed(nodes, collector),
};
if body.ends_with('\n') {
body.pop();
}
if !charset || body.is_ascii() {
return (body, 0);
}
match style {
OutputStyle::Expanded => {
const PREFIX: &str = "@charset \"UTF-8\";\n";
(format!("{PREFIX}{body}"), PREFIX.len())
}
OutputStyle::Compressed => {
let prefix = "\u{FEFF}";
(format!("{prefix}{body}"), prefix.len())
}
}
}
fn record(out: &str, lines: SrcLines, collector: &mut Option<SmCollector>) {
if let Some(c) = collector {
let file = if lines.map_file != 0 {
lines.map_file
} else {
lines.file
};
let line = if lines.map_line != 0 {
lines.map_line
} else {
lines.start
};
if line == 0 {
return;
}
c.record(out.len(), file, line - 1, lines.start_col);
}
}
fn emit_expanded(nodes: &[OutNode], collector: &mut Option<SmCollector>) -> String {
let mut out = String::new();
let mut prev = SrcLines::default();
for node in nodes {
emit_node_expanded(&mut out, node, 0, &mut prev, collector);
}
out
}
fn is_trailing(comment: SrcLines, prev: SrcLines) -> bool {
comment.file != 0 && comment.file == prev.file && comment.start == prev.end && comment != prev
}
fn block_start(lines: SrcLines) -> SrcLines {
SrcLines {
file: lines.file,
start: lines.start,
end: lines.start,
col: 0,
start_col: 0,
map_file: 0,
map_line: 0,
}
}
fn push_trailing_comment(out: &mut String, text: &str, lines: SrcLines, collector: &mut Option<SmCollector>) {
while out.ends_with('\n') {
out.pop();
}
out.push(' ');
record(out, lines, collector);
out.push_str("/*");
push_comment_text(out, text, "", lines.start_col as usize);
out.push_str("*/\n");
}
fn close_block(out: &mut String, indent: &str, children: usize, last_joined: bool) {
if children == 1 && last_joined {
out.pop(); out.push_str(" }\n");
} else {
out.push_str(indent);
out.push_str("}\n");
}
}
fn indent_for(depth: usize) -> std::borrow::Cow<'static, str> {
const PAD: &str = " "; match PAD.get(..depth * 2) {
Some(s) => std::borrow::Cow::Borrowed(s),
None => std::borrow::Cow::Owned(" ".repeat(depth)),
}
}
fn emit_node_expanded(
out: &mut String,
node: &OutNode,
depth: usize,
prev: &mut SrcLines,
collector: &mut Option<SmCollector>,
) -> bool {
let indent = indent_for(depth);
let indent = indent.as_ref();
match node {
OutNode::ModuleScope { nodes, .. } => {
let mut joined = false;
for n in nodes {
joined = emit_node_expanded(out, n, depth, prev, collector);
}
return joined;
}
OutNode::Rule {
selectors,
linebreaks,
items,
lines,
..
} => {
let selectors = selectors.to_strings();
out.push_str(indent);
record(out, *lines, collector);
for (i, sel) in selectors.iter().enumerate() {
if i > 0 {
out.push(',');
if linebreaks.get(i).copied().unwrap_or(false) {
out.push('\n');
out.push_str(indent);
} else {
out.push(' ');
}
}
if sel.contains('\n') && !indent.is_empty() {
let mut first = true;
for line in sel.split('\n') {
if !first {
out.push('\n');
out.push_str(indent);
}
out.push_str(line);
first = false;
}
} else {
out.push_str(sel);
}
}
out.push_str(" {\n");
let mut inner = block_start(*lines);
let mut joined = false;
for item in items {
joined = emit_item_expanded(out, item, depth + 1, &mut inner, collector);
}
close_block(out, indent, items.len(), joined);
*prev = *lines;
}
OutNode::Comment(text, lines) => {
if is_trailing(*lines, *prev) {
push_trailing_comment(out, text, *lines, collector);
*prev = *lines;
return true;
}
out.push_str(indent);
record(out, *lines, collector);
out.push_str("/*");
push_comment_text(out, text, indent, lines.start_col as usize);
out.push_str("*/\n");
*prev = *lines;
}
OutNode::Raw(s) => {
out.push_str(indent);
out.push_str(s);
out.push('\n');
*prev = SrcLines::default();
}
OutNode::GroupEnd | OutNode::MediaHoist | OutNode::AtRootHoist { .. } | OutNode::AtRootPackTight => {
return false;
}
OutNode::Blank => {
out.push('\n');
}
OutNode::AtDecl {
prop,
value,
important,
custom,
lines,
} => {
out.push_str(indent);
record(out, *lines, collector);
out.push_str(prop);
emit_decl_value_expanded(out, value, *important, *custom, lines.col as usize, indent);
out.push_str(";\n");
*prev = *lines;
}
OutNode::AtRule {
name,
prelude,
body,
has_block,
lines,
} => {
out.push_str(indent);
record(out, *lines, collector);
out.push('@');
out.push_str(name);
if !prelude.is_empty() {
out.push(' ');
out.push_str(prelude);
}
*prev = *lines;
if !has_block {
out.push_str(";\n");
return false;
}
if body.is_empty() {
out.push_str(" {}\n");
return false;
}
out.push_str(" {\n");
let mut inner = block_start(*lines);
let mut children = 0usize;
let mut joined = false;
for child in body {
let before = out.len();
let j = emit_node_expanded(out, child, depth + 1, &mut inner, collector);
if out.len() > before {
children += 1;
joined = j;
}
}
close_block(out, indent, children, joined);
}
}
false
}
fn emit_item_expanded(
out: &mut String,
item: &OutItem,
depth: usize,
prev: &mut SrcLines,
collector: &mut Option<SmCollector>,
) -> bool {
let indent = indent_for(depth);
let indent = indent.as_ref();
match item {
OutItem::Decl {
prop,
value,
important,
custom,
lines,
} => {
out.push_str(indent);
record(out, *lines, collector);
out.push_str(prop);
emit_decl_value_expanded(out, value, *important, *custom, lines.col as usize, indent);
out.push_str(";\n");
*prev = *lines;
}
OutItem::Comment(text, lines) => {
if is_trailing(*lines, *prev) {
push_trailing_comment(out, text, *lines, collector);
*prev = *lines;
return true;
}
out.push_str(indent);
record(out, *lines, collector);
out.push_str("/*");
push_comment_text(out, text, indent, lines.start_col as usize);
out.push_str("*/\n");
*prev = *lines;
}
OutItem::ChildlessAtRule { name, prelude, lines } => {
out.push_str(indent);
record(out, *lines, collector);
out.push('@');
out.push_str(name);
if !prelude.is_empty() {
out.push(' ');
out.push_str(prelude);
}
out.push_str(";\n");
*prev = *lines;
}
OutItem::NestedRule { selectors, items } => {
out.push_str(indent);
out.push_str(&selectors.join(", "));
out.push_str(" {\n");
let mut inner = SrcLines::default();
let mut joined = false;
for child in items {
joined = emit_item_expanded(out, child, depth + 1, &mut inner, collector);
}
close_block(out, indent, items.len(), joined);
*prev = SrcLines::default();
}
OutItem::NestedAtRule { name, prelude, items } => {
out.push_str(indent);
out.push('@');
out.push_str(name);
if !prelude.is_empty() {
out.push(' ');
out.push_str(prelude);
}
out.push_str(" {\n");
let mut inner = SrcLines::default();
let mut joined = false;
for child in items {
joined = emit_item_expanded(out, child, depth + 1, &mut inner, collector);
}
close_block(out, indent, items.len(), joined);
*prev = SrcLines::default();
}
}
false
}
fn emit_decl_value_expanded(
out: &mut String,
value: &str,
important: bool,
custom: bool,
name_col: usize,
indent: &str,
) {
if custom {
out.push(':');
match minimum_indentation(value) {
MinIndent::SingleLine => out.push_str(value),
MinIndent::Trailing => {
out.push_str(trim_ascii_right_exclude_escape(value));
out.push(' ');
}
MinIndent::Min(m) => write_with_indent(out, value, m.min(name_col), indent),
}
return;
}
out.push_str(": ");
out.push_str(value);
if important {
out.push_str(" !important");
}
}
enum MinIndent {
SingleLine,
Trailing,
Min(usize),
}
fn minimum_indentation(text: &str) -> MinIndent {
let bytes = text.as_bytes();
let mut i = match text.find('\n') {
None => return MinIndent::SingleLine,
Some(p) => p + 1,
};
if i >= bytes.len() {
return MinIndent::Trailing;
}
let mut min: Option<usize> = None;
while i < bytes.len() {
let start = i;
while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') {
i += 1;
}
if i >= bytes.len() {
break; }
if bytes[i] == b'\n' {
i += 1; continue;
}
let col = i - start;
min = Some(min.map_or(col, |m| m.min(col)));
while i < bytes.len() && bytes[i] != b'\n' {
i += 1;
}
i += 1;
}
match min {
None => MinIndent::Trailing,
Some(m) => MinIndent::Min(m),
}
}
fn trim_ascii_right_exclude_escape(s: &str) -> &str {
let bytes = s.as_bytes();
let mut end = bytes.len();
while end > 0 && matches!(bytes[end - 1], b' ' | b'\t' | b'\n' | b'\r' | b'\x0c') {
end -= 1;
}
if end != 0 && end != bytes.len() && bytes[end - 1] == b'\\' {
end += 1;
}
&s[..end]
}
fn write_with_indent(out: &mut String, text: &str, min_indent: usize, indent: &str) {
let first_end = text.find('\n').unwrap_or(text.len());
out.push_str(&text[..first_end]);
if first_end == text.len() {
return;
}
let mut i = first_end + 1;
loop {
let mut line_start = i;
let mut newlines = 1usize;
loop {
if i >= text.len() {
out.push(' ');
return;
}
match text.as_bytes()[i] {
b' ' | b'\t' => i += 1,
b'\n' => {
i += 1;
line_start = i;
newlines += 1;
}
_ => break,
}
}
for _ in 0..newlines {
out.push('\n');
}
out.push_str(indent);
let line_end = text[i..].find('\n').map(|p| i + p).unwrap_or(text.len());
out.push_str(&text[line_start + min_indent..line_end]);
if line_end == text.len() {
return;
}
i = line_end + 1;
}
}
fn emit_compressed(nodes: &[OutNode], collector: &mut Option<SmCollector>) -> String {
let mut out = String::new();
for node in nodes {
emit_node_compressed(&mut out, node, collector);
}
out
}
fn emit_compressed_body(out: &mut String, nodes: &[OutNode], collector: &mut Option<SmCollector>) {
let mut prev_was_decl = false;
for node in nodes {
if matches!(node, OutNode::Comment(..) | OutNode::Blank) {
continue;
}
if prev_was_decl {
out.push(';');
}
emit_node_compressed(out, node, collector);
prev_was_decl = matches!(node, OutNode::AtDecl { .. });
}
}
fn compressed_nested_rule(selectors: &[String], items: &[OutItem]) -> String {
let inner: Vec<String> = items
.iter()
.filter_map(|it| match it {
OutItem::Decl {
prop,
value,
important,
custom,
..
} => {
let imp = if *important && !*custom { "!important" } else { "" };
let value = fold_value_compressed(value, *custom);
Some(format!("{prop}:{value}{imp}"))
}
OutItem::Comment(..) => None,
OutItem::ChildlessAtRule { name, prelude, .. } if prelude.is_empty() => Some(format!("@{name}")),
OutItem::ChildlessAtRule { name, prelude, .. } => Some(format!("@{name} {prelude}")),
OutItem::NestedRule { selectors, items } => Some(compressed_nested_rule(selectors, items)),
OutItem::NestedAtRule { name, prelude, items } => {
Some(compressed_nested_at_rule(name, prelude, items))
}
})
.collect();
format!("{}{{{}}}", selectors.join(","), inner.join(";"))
}
fn fold_value_compressed<'v>(value: &'v str, custom: bool) -> std::borrow::Cow<'v, str> {
if !custom || !value.contains('\n') {
return std::borrow::Cow::Borrowed(value);
}
let mut out = String::with_capacity(value.len());
let mut chars = value.chars().peekable();
while let Some(c) = chars.next() {
if c != '\n' {
out.push(c);
continue;
}
out.push(' ');
while matches!(chars.peek(), Some(' ' | '\t' | '\n' | '\r' | '\x0c')) {
chars.next();
}
}
std::borrow::Cow::Owned(out)
}
fn compressed_at_rule_omits_space(name: &str, prelude: &str) -> bool {
matches!(name, "media" | "supports") && prelude.starts_with('(')
}
fn compressed_nested_at_rule(name: &str, prelude: &str, items: &[OutItem]) -> String {
let body = compressed_nested_rule(&[], items);
if prelude.is_empty() {
format!("@{name}{body}")
} else if compressed_at_rule_omits_space(name, prelude) {
format!("@{name}{prelude}{body}")
} else {
format!("@{name} {prelude}{body}")
}
}
fn emit_node_compressed(out: &mut String, node: &OutNode, collector: &mut Option<SmCollector>) {
match node {
OutNode::ModuleScope { nodes, .. } => {
for n in nodes {
emit_node_compressed(out, n, collector);
}
}
OutNode::Rule {
selectors,
linebreaks: _,
items,
lines,
..
} => {
let decls: Vec<(String, Option<SrcLines>)> = items
.iter()
.filter_map(|it| match it {
OutItem::Decl {
prop,
value,
important,
custom,
lines,
} => {
let imp = if *important && !*custom { "!important" } else { "" };
let value = fold_value_compressed(value, *custom);
Some((format!("{prop}:{value}{imp}"), Some(*lines)))
}
OutItem::Comment(..) => None,
OutItem::ChildlessAtRule { name, prelude, lines } => {
let s = if prelude.is_empty() {
format!("@{name}")
} else {
format!("@{name} {prelude}")
};
Some((s, Some(*lines)))
}
OutItem::NestedRule { selectors, items } => {
Some((compressed_nested_rule(selectors, items), None))
}
OutItem::NestedAtRule { name, prelude, items } => {
Some((compressed_nested_at_rule(name, prelude, items), None))
}
})
.collect();
if decls.is_empty() {
return;
}
record(out, *lines, collector);
out.push_str(&selectors.to_strings().join(","));
out.push('{');
for (i, (s, dlines)) in decls.iter().enumerate() {
if i > 0 {
out.push(';');
}
if let Some(l) = dlines {
record(out, *l, collector);
}
out.push_str(s);
}
out.push('}');
}
OutNode::Comment(..) => {}
OutNode::Raw(s) => out.push_str(s),
OutNode::Blank => {}
OutNode::GroupEnd | OutNode::MediaHoist | OutNode::AtRootHoist { .. } | OutNode::AtRootPackTight => {}
OutNode::AtDecl {
prop,
value,
important,
custom,
lines,
} => {
let imp = if *important && !*custom { "!important" } else { "" };
record(out, *lines, collector);
out.push_str(prop);
out.push(':');
out.push_str(&fold_value_compressed(value, *custom));
out.push_str(imp);
}
OutNode::AtRule {
name,
prelude,
body,
has_block,
lines,
} => {
record(out, *lines, collector);
out.push('@');
out.push_str(name);
if !prelude.is_empty() {
if !compressed_at_rule_omits_space(name, prelude) {
out.push(' ');
}
out.push_str(prelude);
}
if !has_block {
out.push(';');
return;
}
out.push('{');
emit_compressed_body(out, body, collector);
out.push('}');
}
}
}
fn push_comment_text(out: &mut String, text: &str, indent: &str, start_col: usize) {
if !text.contains('\n') {
out.push_str(text);
return;
}
let lines: Vec<&str> = text.split('\n').collect();
let mut min: Option<usize> = None;
for (i, line) in lines.iter().enumerate().skip(1) {
if i + 1 != lines.len() && line.trim().is_empty() {
continue;
}
let ind = line.len() - line.trim_start_matches([' ', '\t']).len();
min = Some(min.map_or(ind, |m| m.min(ind)));
}
let strip = min.map_or(0, |m| m.min(start_col));
out.push_str(lines[0]);
for (i, line) in lines.iter().enumerate().skip(1) {
out.push('\n');
if i + 1 != lines.len() && line.trim().is_empty() {
continue;
}
let ind = line.len() - line.trim_start_matches([' ', '\t']).len();
out.push_str(indent);
out.push_str(&line[strip.min(ind)..]);
}
}