use crate::grammar::{Alt, Grammar, RuleDef};
use crate::lex::{self, Error, Lex, Tin, Token};
use crate::value::{Node, Value};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Options {
pub lowercase_properties: bool,
pub position: bool,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Phase {
Open,
Close,
}
struct Frame<'g> {
name: &'g str,
phase: Phase,
node: Option<Node>,
inherited: bool,
o: Vec<Token>,
c: Vec<Token>,
child: Option<Node>,
}
impl<'g> Frame<'g> {
fn new(name: &'g str, node: Option<Node>, inherited: bool) -> Frame<'g> {
Frame {
name,
phase: Phase::Open,
node,
inherited,
o: Vec::new(),
c: Vec::new(),
child: None,
}
}
}
const MAX_IDLE_STEPS: usize = 64;
pub fn parse(grammar: &Grammar, src: &str, options: Options) -> Result<Value, Error> {
if src.is_empty() {
let mut sheet = Node::new();
sheet.set("type", "stylesheet");
sheet.set("rules", Value::List(Vec::new()));
return Ok(Value::Node(sheet));
}
Machine {
grammar,
lex: Lex::new(src, options.lowercase_properties),
options,
stack: vec![Frame::new("stylesheet", None, false)],
idle: 0,
}
.run()
}
struct Machine<'g> {
grammar: &'g Grammar,
lex: Lex,
options: Options,
stack: Vec<Frame<'g>>,
idle: usize,
}
impl<'g> Machine<'g> {
fn run(mut self) -> Result<Value, Error> {
loop {
let top = self.stack.len() - 1;
let name: &'g str = self.stack[top].name;
let phase = self.stack[top].phase;
let grammar: &'g Grammar = self.grammar;
let def: &'g RuleDef = grammar.rule(name).ok_or_else(|| Error {
code: "unexpected".to_string(),
message: format!("no grammar rule named '{name}'"),
line: self.lex.point().r_i,
column: self.lex.point().c_i,
})?;
let alts: &'g [Alt] = match phase {
Phase::Open => &def.open,
Phase::Close => &def.close,
};
let mut chosen: Option<&'g Alt> = None;
for alt in alts {
let mut matched = true;
for (i, want) in alt.s.iter().enumerate() {
let seen = self.lex.peek(i + 1, name)?;
if want != seen[i].tin.name() {
matched = false;
break;
}
}
if matched {
chosen = Some(alt);
break;
}
}
let Some(alt) = chosen else {
return Err(self.no_alt(name, phase));
};
let matched: Vec<Token> = self.lex.peek(alt.s.len(), name)?.to_vec();
let consume = alt.s.len().saturating_sub(alt.b);
self.lex.consume(consume);
if 0 < consume {
self.idle = 0;
} else {
self.idle += 1;
if MAX_IDLE_STEPS < self.idle {
return Err(self.stuck(name));
}
}
match phase {
Phase::Open => self.stack[top].o = matched,
Phase::Close => self.stack[top].c = matched,
}
for action in &alt.a {
self.act(action);
}
if let Some(child) = &alt.p {
let node = self.stack[top].node.take();
let inherited = node.is_some();
self.stack[top].phase = Phase::Close;
let child = Frame::new(child, node, inherited);
self.stack.push(child);
} else if let Some(replacement) = &alt.r {
let node = self.stack[top].node.take();
let inherited = self.stack[top].inherited;
self.stack[top] = Frame::new(replacement, node, inherited);
} else if Phase::Open == phase {
self.stack[top].phase = Phase::Close;
} else {
let done = self.stack.pop().expect("stack is never empty here");
let Some(parent) = self.stack.last_mut() else {
return Ok(done.node.map(Value::Node).unwrap_or(Value::Undefined));
};
if done.inherited {
parent.node = done.node;
} else {
parent.child = done.node;
}
}
}
}
fn no_alt(&mut self, name: &str, phase: Phase) -> Error {
let point = self.lex.point();
let (line, column, what) = match self.lex.peek(1, name) {
Ok(seen) => {
let tok = &seen[0];
let what = if Tin::Zz == tok.tin {
"end of input".to_string()
} else {
format!("{:?}", tok.val)
};
(tok.r_i, tok.c_i, what)
}
Err(err) => return err,
};
Error {
code: "unexpected".to_string(),
message: format!(
"unexpected character(s): {what} (no '{name}' {} alternative matches)",
if Phase::Open == phase {
"open"
} else {
"close"
}
),
line: if 0 < line { line } else { point.r_i },
column: if 0 < column { column } else { point.c_i },
}
}
fn stuck(&self, name: &str) -> Error {
let point = self.lex.point();
Error {
code: "unexpected".to_string(),
message: format!(
"the grammar made no progress at rule '{name}' after \
{MAX_IDLE_STEPS} steps without consuming a token"
),
line: point.r_i,
column: point.c_i,
}
}
fn act(&mut self, action: &str) {
match action {
"@cssSheet" => {
let mut node = Node::new();
node.set("type", "stylesheet");
node.set("rules", Value::List(Vec::new()));
if self.options.position {
node.set("position", position(Some((1, 1)), None));
}
self.set_node(node);
}
"@cssRule" => {
let mut node = Node::new();
node.set("type", "rule");
node.set("selectors", Value::List(Vec::new()));
node.set("declarations", Value::List(Vec::new()));
self.with_pos(node, false);
}
"@cssDecl" => {
let property = self.open_val();
let mut node = Node::new();
node.set("type", "declaration");
node.set("property", property);
node.set("value", "");
self.with_pos(node, false);
}
"@cssComment" => {
let comment = self.open_val();
let mut node = Node::new();
node.set("type", "comment");
node.set("comment", comment);
self.with_pos(node, true);
}
"@cssKeyframe" => {
let mut node = Node::new();
node.set("type", "keyframe");
node.set("values", Value::List(Vec::new()));
node.set("declarations", Value::List(Vec::new()));
self.with_pos(node, false);
}
"@cssAtRules" => {
let node = self.open_token().map(make_at_rules).unwrap_or_default();
self.with_pos(node, false);
}
"@cssAtDecls" => {
let node = self.open_token().map(make_at_decls).unwrap_or_default();
self.with_pos(node, false);
}
"@cssKeyframes" => {
let node = self.open_token().map(make_keyframes).unwrap_or_default();
self.with_pos(node, false);
}
"@cssAtStmt" => {
let node = self.open_token().map(make_at_stmt).unwrap_or_default();
self.with_pos(node, true);
}
"@cssSelector" => {
let v = self.open_val();
if let Some(node) = self.node_mut() {
node.push_to("selectors", Value::Str(v));
}
}
"@cssKfValue" => {
let v = self.open_val();
if let Some(node) = self.node_mut() {
node.push_to("values", Value::Str(v));
}
}
"@cssDeclVal" => {
let v = self.open_val();
let end = self
.options
.position
.then(|| self.open_token().map(lex::end_pos))
.flatten();
if let Some(node) = self.node_mut() {
node.set("value", Value::Str(v));
if let Some(end) = end {
set_end(node, end);
}
}
}
"@cssEnd" => {
if !self.options.position {
return;
}
let end = self.close_token().map(lex::end_pos);
if let (Some(end), Some(node)) = (end, self.node_mut()) {
set_end(node, end);
}
}
"@cssPushRule" => self.push_child("rules"),
"@cssPushDecl" => self.push_child("declarations"),
"@cssPushKf" => self.push_child("keyframes"),
_ => {}
}
}
fn top(&mut self) -> &mut Frame<'g> {
self.stack.last_mut().expect("stack is never empty")
}
fn open_token(&self) -> Option<&Token> {
self.stack.last().and_then(|f| f.o.first())
}
fn close_token(&self) -> Option<&Token> {
self.stack.last().and_then(|f| f.c.first())
}
fn open_val(&self) -> String {
self.open_token().map(|t| t.val.clone()).unwrap_or_default()
}
fn node_mut(&mut self) -> Option<&mut Node> {
self.top().node.as_mut()
}
fn set_node(&mut self, node: Node) {
let top = self.stack.len() - 1;
if self.stack[top].inherited {
let borrowed = self.stack[top].node.take();
self.stack[top].inherited = false;
if 0 < top {
self.stack[top - 1].node = borrowed;
}
}
self.stack[top].node = Some(node);
}
fn with_pos(&mut self, mut node: Node, end: bool) {
if self.options.position {
if let Some(tok) = self.open_token() {
let start = lex::start_pos(tok);
let stop = end.then(|| lex::end_pos(tok));
node.set("position", position(Some(start), stop));
}
}
self.set_node(node);
}
fn push_child(&mut self, field: &str) {
let top = self.stack.len() - 1;
let Some(child) = self.stack[top].child.take() else {
return;
};
if let Some(node) = self.stack[top].node.as_mut() {
node.push_to(field, Value::Node(child));
}
}
}
fn position(start: Option<(usize, usize)>, end: Option<(usize, usize)>) -> Value {
let mut pos = Node::new();
if let Some((line, column)) = start {
pos.set("start", point(line, column));
}
match end {
Some((line, column)) => pos.set("end", point(line, column)),
None => pos.set("end", Value::Undefined),
}
Value::Node(pos)
}
fn point(line: usize, column: usize) -> Value {
let mut p = Node::new();
p.set("line", Value::Num(line as f64));
p.set("column", Value::Num(column as f64));
Value::Node(p)
}
fn set_end(node: &mut Node, (line, column): (usize, usize)) {
if let Some(Value::Node(pos)) = node.get_mut("position") {
pos.set("end", point(line, column));
}
}
fn make_at_rules(tok: &Token) -> Node {
let kw = tok.val.as_str();
let prelude = tok.use_text.as_str();
let mut node = Node::new();
match kw {
"media" => {
node.set("type", "media");
node.set("media", prelude);
}
"supports" => {
node.set("type", "supports");
node.set("supports", prelude);
}
"host" => {
node.set("type", "host");
}
_ if "document" == kw || kw.ends_with("-document") => {
node.set("type", "document");
node.set("document", prelude);
node.set("vendor", lex::vendor_prefix(kw).unwrap_or(""));
}
_ => {
node.set("type", kw);
node.set(kw, prelude);
}
}
node.set("rules", Value::List(Vec::new()));
node
}
fn make_at_decls(tok: &Token) -> Node {
let kw = tok.val.as_str();
let mut node = Node::new();
node.set("type", kw);
if "page" == kw {
let selectors = lex::split_selectors(&tok.use_text)
.into_iter()
.map(Value::Str)
.collect();
node.set("selectors", Value::List(selectors));
}
node.set("declarations", Value::List(Vec::new()));
node
}
fn make_keyframes(tok: &Token) -> Node {
let kw = tok.val.as_str();
let mut node = Node::new();
node.set("type", "keyframes");
node.set("name", tok.use_text.as_str());
if let Some(vendor) = lex::vendor_prefix(kw) {
node.set("vendor", vendor);
}
node.set("keyframes", Value::List(Vec::new()));
node
}
fn make_at_stmt(tok: &Token) -> Node {
let kw = tok.val.as_str();
let params = tok.use_text.as_str();
let mut node = Node::new();
if "custom-media" == kw {
if let Some((name, media)) = split_custom_media(params) {
node.set("type", "custom-media");
node.set("name", name);
node.set("media", media);
return node;
}
}
node.set("type", kw);
node.set(kw, params);
node
}
fn split_custom_media(params: &str) -> Option<(&str, &str)> {
if !params.starts_with("--") {
return None;
}
let name_end = params.find(lex::es_is_whitespace).unwrap_or(params.len());
if name_end <= 2 {
return None;
}
Some((¶ms[..name_end], lex::es_trim(¶ms[name_end..])))
}