use std::cell::RefCell;
use std::rc::Rc;
use std::sync::{Arc, OnceLock};
use tabnas::{ActionError, Context, GrammarSetting, PluginError, Rule, Tabnas, Token, Value};
use crate::grammar::{specs, RULES};
use crate::lex::{
self, col_width, es_is_whitespace, es_trim, split_selectors, vendor_prefix, BRACE_SCAN,
OVERSHOOT,
};
use crate::Options;
pub const TREE_RULE_DEPTH: usize = 768;
const DEPTH_GUARD: &str = "tabnas-css/depth";
const ARENA: &str = "tabnas-css/arena";
const NODES: &str = "tabnas-css/nodes";
const ASTRAL: &str = "tabnas-css/astral";
pub(crate) fn arena_meta() -> Value {
arena_marker().clone()
}
fn arena_marker() -> &'static Value {
static META: OnceLock<Value> = OnceLock::new();
META.get_or_init(|| {
let mut meta = object();
put(&mut meta, ARENA, Value::Bool(true));
meta
})
}
fn arena(ctx: &Context) -> bool {
match (&ctx.meta, arena_marker()) {
(Value::Object(meta), Value::Object(ours)) => Arc::ptr_eq(meta, ours),
_ => false,
}
}
fn object() -> Value {
Value::object(Default::default())
}
fn list() -> Value {
Value::array(Vec::new())
}
fn text(s: &str) -> Value {
Value::String(s.to_string())
}
fn put(target: &mut Value, key: &str, value: Value) {
let Some(fields) = target.as_object_mut() else {
return;
};
match array_index(key) {
Some(index) if !fields.contains_key(key) => {
let at = fields
.keys()
.take_while(|k| array_index(k).is_some_and(|other| other < index))
.count();
fields.shift_insert(at, key.to_string(), value);
}
_ => {
fields.insert(key.to_string(), value);
}
}
}
fn array_index(key: &str) -> Option<u32> {
let canonical = !key.is_empty()
&& key.bytes().all(|b| b.is_ascii_digit())
&& (key == "0" || !key.starts_with('0'));
if !canonical {
return None;
}
key.parse::<u32>().ok().filter(|&index| index < u32::MAX)
}
fn push_to(target: &mut Value, field: &str, item: Value) {
let Some(fields) = target.as_object_mut() else {
return;
};
match fields.get_mut(field) {
Some(Value::Array(items)) => Arc::make_mut(items).push(item),
_ => {
fields.insert(field.to_string(), Value::array(vec![item]));
}
}
}
fn nodes(ctx: &mut Context) -> &mut Vec<Value> {
let slot = ctx.u.entry(NODES.to_string()).or_insert_with(list);
if slot.as_array_mut().is_none() {
*slot = list();
}
slot.as_array_mut().expect("the arena was just made a list")
}
fn install_node(rule: &mut Rule, ctx: &mut Context, record: Value) {
let node = if arena(ctx) {
let records = nodes(ctx);
let id = records.len();
records.push(record);
Value::Number(id as f64)
} else {
record
};
rule.node = Rc::new(RefCell::new(node));
}
fn with_node(rule: &Rule, ctx: &mut Context, edit: impl FnOnce(&mut Value)) {
if arena(ctx) {
let id = match *rule.node.borrow() {
Value::Number(id) => id as usize,
_ => return,
};
if let Some(record) = nodes(ctx).get_mut(id) {
edit(record);
}
} else {
edit(&mut rule.node.borrow_mut());
}
}
fn col16(ctx: &mut Context, pos: usize, ci: usize) -> usize {
if !ctx.u.contains_key(ASTRAL) {
let wide: Vec<Value> = if ctx.source.bytes().any(|b| 0xF0 <= b) {
ctx.source
.chars()
.enumerate()
.filter(|(_, c)| 2 == c.len_utf16())
.map(|(i, _)| Value::Number(i as f64))
.collect()
} else {
Vec::new()
};
ctx.u.insert(ASTRAL.to_string(), Value::array(wide));
}
let Some(Value::Array(wide)) = ctx.u.get(ASTRAL) else {
return ci;
};
if wide.is_empty() {
return ci;
}
let at = |v: &Value| match v {
Value::Number(n) => *n as usize,
_ => 0,
};
let from = pos.saturating_sub(ci.saturating_sub(1));
let low = wide.partition_point(|v| at(v) < from);
let high = wide.partition_point(|v| at(v) < pos);
ci + (high - low)
}
fn point(line: usize, column: usize) -> Value {
let mut at = object();
put(&mut at, "line", Value::Number(line as f64));
put(&mut at, "column", Value::Number(column as f64));
at
}
fn start_pos(ctx: &mut Context, token: &Token) -> Value {
let column = col16(ctx, token.site.pos, token.site.ci);
point(token.site.ri, column)
}
fn end_pos(ctx: &mut Context, token: &Token) -> Value {
let src = token.src.as_str();
match src.rfind('\n') {
Some(last) => point(
token.site.ri + src.matches('\n').count(),
1 + col_width(&src[last + 1..]),
),
None => {
let column = col16(ctx, token.site.pos, token.site.ci);
point(token.site.ri, column + col_width(src))
}
}
}
fn with_pos(rule: &Rule, ctx: &mut Context, position: bool, end: bool) -> Option<Value> {
if !position {
return None;
}
let token = rule.o0()?;
let mut at = object();
put(&mut at, "start", start_pos(ctx, token));
if end {
put(&mut at, "end", end_pos(ctx, token));
}
Some(at)
}
fn set_end(rule: &Rule, ctx: &mut Context, end: Value) {
with_node(rule, ctx, |record| {
if let Some(at) = record.as_object_mut().and_then(|f| f.get_mut("position")) {
put(at, "end", end);
}
});
}
fn token_text(token: &Token) -> String {
match &token.val {
Value::String(s) => s.clone(),
_ => String::new(),
}
}
fn use_text(token: &Token, key: &str) -> String {
match token.use_data().get(key) {
Some(Value::String(s)) => s.clone(),
_ => String::new(),
}
}
fn make_at_rules(token: &Token) -> Value {
let kw = token_text(token);
let prelude = use_text(token, "prelude");
let mut node = object();
match kw.as_str() {
"host" => put(&mut node, "type", text("host")),
_ if "document" == kw || kw.ends_with("-document") => {
put(&mut node, "type", text("document"));
put(&mut node, "document", text(&prelude));
put(&mut node, "vendor", text(vendor_prefix(&kw).unwrap_or("")));
}
_ => {
put(&mut node, "type", text(&kw));
put(&mut node, &kw, text(&prelude));
}
}
put(&mut node, "rules", list());
node
}
fn make_at_decls(token: &Token) -> Value {
let kw = token_text(token);
let mut node = object();
put(&mut node, "type", text(&kw));
if "page" == kw {
let selectors = split_selectors(&use_text(token, "prelude"))
.into_iter()
.map(Value::String)
.collect();
put(&mut node, "selectors", Value::array(selectors));
}
put(&mut node, "declarations", list());
node
}
fn make_keyframes(token: &Token) -> Value {
let kw = token_text(token);
let mut node = object();
put(&mut node, "type", text("keyframes"));
put(&mut node, "name", text(&use_text(token, "prelude")));
if let Some(vendor) = vendor_prefix(&kw) {
put(&mut node, "vendor", text(vendor));
}
put(&mut node, "keyframes", list());
node
}
fn make_at_stmt(token: &Token) -> Value {
let kw = token_text(token);
let params = use_text(token, "params");
let mut node = object();
if "custom-media" == kw && params.starts_with("--") {
let name_end = params.find(es_is_whitespace).unwrap_or(params.len());
if 2 < name_end {
put(&mut node, "type", text("custom-media"));
put(&mut node, "name", text(¶ms[..name_end]));
put(&mut node, "media", text(es_trim(¶ms[name_end..])));
return node;
}
}
put(&mut node, "type", text(&kw));
put(&mut node, &kw, text(¶ms));
node
}
type Outcome = Result<(), ActionError>;
fn o0_text(rule: &Rule) -> String {
rule.o0().map(token_text).unwrap_or_default()
}
fn push_child(rule: &Rule, ctx: &mut Context, field: &str) {
if rule.child_node.is_undefined() {
return;
}
let child = rule.child_node.clone();
with_node(rule, ctx, |record| push_to(record, field, child));
}
fn register_actions(parser: &mut Tabnas, position: bool) {
parser.action_with_context("@cssSheet", move |rule, ctx| -> Outcome {
let mut node = object();
put(&mut node, "type", text("stylesheet"));
put(&mut node, "rules", list());
if position {
let mut at = object();
put(&mut at, "start", point(1, 1));
put(&mut node, "position", at);
}
install_node(rule, ctx, node);
Ok(())
});
type Build = fn(&Rule) -> Value;
let simple: [(&str, Build, bool); 4] = [
(
"@cssRule",
|_| {
let mut node = object();
put(&mut node, "type", text("rule"));
put(&mut node, "selectors", list());
put(&mut node, "declarations", list());
node
},
false,
),
(
"@cssDecl",
|rule| {
let mut node = object();
put(&mut node, "type", text("declaration"));
put(&mut node, "property", Value::String(o0_text(rule)));
put(&mut node, "value", text(""));
node
},
false,
),
(
"@cssComment",
|rule| {
let mut node = object();
put(&mut node, "type", text("comment"));
put(&mut node, "comment", Value::String(o0_text(rule)));
node
},
true,
),
(
"@cssKeyframe",
|_| {
let mut node = object();
put(&mut node, "type", text("keyframe"));
put(&mut node, "values", list());
put(&mut node, "declarations", list());
node
},
false,
),
];
for (name, build, end) in simple {
parser.action_with_context(name, move |rule, ctx| -> Outcome {
let mut node = build(rule);
if let Some(at) = with_pos(rule, ctx, position, end) {
put(&mut node, "position", at);
}
install_node(rule, ctx, node);
Ok(())
});
}
type AtBuild = fn(&Token) -> Value;
let at_rules: [(&str, AtBuild, bool); 4] = [
("@cssAtRules", make_at_rules, false),
("@cssAtDecls", make_at_decls, false),
("@cssKeyframes", make_keyframes, false),
("@cssAtStmt", make_at_stmt, true),
];
for (name, build, end) in at_rules {
parser.action_with_context(name, move |rule, ctx| -> Outcome {
let mut node = rule.o0().map(build).unwrap_or_else(object);
if let Some(at) = with_pos(rule, ctx, position, end) {
put(&mut node, "position", at);
}
install_node(rule, ctx, node);
Ok(())
});
}
for (name, field) in [("@cssSelector", "selectors"), ("@cssKfValue", "values")] {
parser.action_with_context(name, move |rule, ctx| -> Outcome {
let value = Value::String(o0_text(rule));
with_node(rule, ctx, |record| push_to(record, field, value));
Ok(())
});
}
parser.action_with_context("@cssDeclVal", move |rule, ctx| -> Outcome {
let value = Value::String(o0_text(rule));
with_node(rule, ctx, |record| put(record, "value", value));
if position {
if let Some(token) = rule.o0() {
let end = end_pos(ctx, token);
set_end(rule, ctx, end);
}
}
Ok(())
});
parser.action_with_context("@cssEnd", move |rule, ctx| -> Outcome {
if position {
if let Some(token) = rule.c0() {
let end = end_pos(ctx, token);
set_end(rule, ctx, end);
}
}
if "stylesheet" == rule.name.as_str() && arena(ctx) {
let records = ctx.u.shift_remove(NODES).unwrap_or_else(list);
rule.node = Rc::new(RefCell::new(records));
}
Ok(())
});
for (name, field) in [
("@cssPushRule", "rules"),
("@cssPushDecl", "declarations"),
("@cssPushKf", "keyframes"),
] {
parser.action_with_context(name, move |rule, ctx| -> Outcome {
push_child(rule, ctx, field);
Ok(())
});
}
}
fn is_css_sheet(action: &str) -> bool {
"@cssSheet" == action || (action.starts_with('@') && action.ends_with(":cssSheet"))
}
pub(crate) fn install(parser: &mut Tabnas, options: &Options) -> Result<(), PluginError> {
let specs = specs().map_err(PluginError)?;
let first = !parser.rule_specs().iter().any(|spec| {
"stylesheet" == spec.name
&& spec
.open
.iter()
.any(|alt| alt.a.iter().any(|action| is_css_sheet(action)))
});
for name in ["#CC", "#ATR", "#ATD", "#ATK", "#ATS", "#GC"] {
parser.token(name);
}
let lower = options.lowercase_properties;
parser.imperative_lex_match_ref("@css-token", move |lexer, rule, ctx| {
lex::css_token(lexer, rule, ctx, lower)
});
register_actions(parser, options.position);
parser.parse_prepare_ref("@css-prepare", |ctx| {
ctx.u.shift_remove(NODES);
ctx.u.shift_remove(ASTRAL);
ctx.u.shift_remove(OVERSHOOT);
ctx.u.shift_remove(BRACE_SCAN);
});
if first {
parser.subscribe_lex(lex::lex_subscriber);
}
parser.remove_parse_guard("depth");
parser.parse_guard(DEPTH_GUARD, |ctx| {
ctx.rule_stack.len() <= TREE_RULE_DEPTH || arena(ctx)
});
if !first {
for name in RULES {
parser.remove_rule(name);
}
}
parser
.grammar(&specs.options)
.map_err(|e| PluginError(e.0))?;
parser
.grammar_with_setting(&specs.rules, &GrammarSetting::groups("css"))
.map_err(|e| PluginError(e.0))?;
Ok(())
}