use std::io::Read;
use bumpalo::Bump;
use clap::Args;
use css_ast::visit::{QueryableNode, Visit, Visitable};
use css_ast::{CssAtomSet, PROPERTY_KIND_VARIANTS, PropertyKind, StyleSheet};
use css_lexer::Lexer;
use css_parse::{Cursor, Parser, ToSpan};
use csskit_ast::{QueryFunctionalPseudoClass, QueryPseudoClass};
use crate::{CliError, CliResult, GlobalConfig, InputArgs};
#[derive(Debug, Args)]
pub struct Tree {
#[command(flatten)]
input: InputArgs,
}
#[derive(Clone, Debug)]
struct NodeInfo {
display: String,
depth: usize,
child_start_idx: usize,
}
struct CollectionVisitor<'a> {
source: &'a str,
nodes: Vec<NodeInfo>,
depth: usize,
}
impl<'a> CollectionVisitor<'a> {
fn new(source: &'a str) -> Self {
Self { source, nodes: Vec::new(), depth: 0 }
}
fn build_display<T: QueryableNode>(&self, node: &T) -> String {
let meta = node.self_metadata();
let mut display = node.node_id().tag_name().to_string();
for &kind in PROPERTY_KIND_VARIANTS {
if !meta.property_kinds.contains(kind) {
continue;
}
if let Some(cursor) = node.get_property(kind) {
let value = self.cursor_to_str(cursor);
let attr_name = match kind {
PropertyKind::Name => "name",
_ => continue,
};
display.push_str(&format!("[{}={}]", attr_name, value));
}
}
for name in QueryPseudoClass::matching_metadata_pseudos(&meta) {
display.push(':');
display.push_str(name);
}
if let Some(size) = QueryFunctionalPseudoClass::matching_size(&meta) {
display.push_str(&format!(":size({})", size));
}
display
}
fn cursor_to_str(&self, cursor: Cursor) -> &str {
let span = cursor.to_span();
&self.source[span.start().0 as usize..span.end().0 as usize]
}
fn find_ancestor_at_depth(&self, start_idx: usize, target_depth: usize) -> Option<usize> {
(0..start_idx).rev().find(|&j| self.nodes[j].depth == target_depth)
}
fn is_last_child(&self, node_idx: usize) -> bool {
let node_depth = self.nodes[node_idx].depth;
if node_depth == 0 {
return false;
}
let Some(parent_idx) = self.find_ancestor_at_depth(node_idx, node_depth - 1) else {
return false;
};
let parent = &self.nodes[parent_idx];
let parent_depth = parent.depth;
let mut last_child_idx = None;
for j in parent.child_start_idx..self.nodes.len() {
if self.nodes[j].depth == parent_depth + 1 {
last_child_idx = Some(j);
} else if self.nodes[j].depth <= parent_depth {
break;
}
}
last_child_idx == Some(node_idx)
}
}
impl<'a> Visit for CollectionVisitor<'a> {
fn visit_queryable_node<T: QueryableNode>(&mut self, node: &T) {
let display = self.build_display(node);
let node_info = NodeInfo { display, depth: self.depth, child_start_idx: self.nodes.len() + 1 };
self.nodes.push(node_info);
self.depth += 1;
}
fn exit_queryable_node<T: QueryableNode>(&mut self, _node: &T) {
self.depth -= 1;
}
}
impl Tree {
pub fn run(&self, _config: GlobalConfig) -> CliResult {
let bump = Bump::default();
for (filename, mut source) in self.input.sources()? {
let mut src = String::new();
source.read_to_string(&mut src)?;
let lexer = Lexer::new(&CssAtomSet::ATOMS, &src);
let mut parser = Parser::new(&bump, &src, lexer);
let result = parser.parse_entirely::<StyleSheet>();
let Some(stylesheet) = result.output.as_ref() else {
for err in result.errors {
eprintln!("{}", crate::commands::format_diagnostic_error(&err, &src, filename));
}
return Err(CliError::ParseFailed);
};
let mut visitor = CollectionVisitor::new(&src);
stylesheet.accept(&mut visitor);
if let Some(root) = visitor.nodes.first() {
println!("{}", root.display);
}
for (i, node) in visitor.nodes.iter().enumerate().skip(1) {
let prefix = self.build_prefix(&visitor, i, node.depth);
println!("{}{}", prefix, node.display);
}
}
Ok(())
}
fn build_prefix(&self, visitor: &CollectionVisitor, node_idx: usize, node_depth: usize) -> String {
let mut prefix = String::new();
for d in 0..node_depth.saturating_sub(1) {
let ancestor_idx = visitor.find_ancestor_at_depth(node_idx, d + 1);
let show_bar = ancestor_idx.is_none_or(|idx| !visitor.is_last_child(idx));
prefix.push_str(if show_bar { "│ " } else { " " });
}
let connector = if visitor.is_last_child(node_idx) { "╰─ " } else { "├─ " };
prefix.push_str(connector);
prefix
}
}