Skip to main content

omena_parser/
extension.rs

1//! Dialect extension hooks for CSS, SCSS, Less, and CSS Modules parsing.
2//!
3//! The parser keeps its core grammar shared and lets this module provide the
4//! dialect-specific at-rule and selector behavior needed by product consumers.
5
6use omena_syntax::{StyleDialect, SyntaxKind};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum TemplatePlaceholderMode {
10    Brace,
11    AtomicIndexed,
12}
13
14pub trait DialectExtension {
15    fn dialect(&self) -> StyleDialect;
16
17    fn template_placeholder(&self) -> Option<TemplatePlaceholderMode> {
18        None
19    }
20
21    fn classify_variable_token(&self, text: &str) -> Option<SyntaxKind> {
22        match self.dialect() {
23            StyleDialect::Css => None,
24            StyleDialect::Scss | StyleDialect::Sass if text.starts_with('$') => {
25                Some(SyntaxKind::ScssVariable)
26            }
27            StyleDialect::Less if text.starts_with('@') => Some(SyntaxKind::LessVariable),
28            StyleDialect::Scss | StyleDialect::Sass | StyleDialect::Less => None,
29        }
30    }
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub struct BuiltinDialectExtension {
35    dialect: StyleDialect,
36}
37
38impl BuiltinDialectExtension {
39    pub const fn new(dialect: StyleDialect) -> Self {
40        Self { dialect }
41    }
42}
43
44impl DialectExtension for BuiltinDialectExtension {
45    fn dialect(&self) -> StyleDialect {
46        self.dialect
47    }
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub(crate) struct AtRuleSpec {
52    pub(crate) node_kind: SyntaxKind,
53    pub(crate) block_kind: AtRuleBlockKind,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub(crate) enum AtRuleBlockKind {
58    GroupRuleList,
59    DeclarationList,
60    Keyframes,
61    Raw,
62}
63
64pub(crate) fn at_rule_spec(text: &str) -> Option<AtRuleSpec> {
65    let lowered = text.to_ascii_lowercase();
66    if at_rule_is_vendor_prefixed_keyframes(&lowered) {
67        return Some(AtRuleSpec {
68            node_kind: SyntaxKind::KeyframesRule,
69            block_kind: AtRuleBlockKind::Keyframes,
70        });
71    }
72    let (node_kind, block_kind) = match lowered.as_str() {
73        "@media" => (SyntaxKind::MediaRule, AtRuleBlockKind::GroupRuleList),
74        "@supports" => (SyntaxKind::SupportsRule, AtRuleBlockKind::GroupRuleList),
75        "@when" => (SyntaxKind::WhenRule, AtRuleBlockKind::GroupRuleList),
76        "@if" => (SyntaxKind::IfRule, AtRuleBlockKind::GroupRuleList),
77        "@else" => (SyntaxKind::ElseRule, AtRuleBlockKind::GroupRuleList),
78        "@container" => (SyntaxKind::ContainerRule, AtRuleBlockKind::GroupRuleList),
79        "@layer" => (SyntaxKind::LayerRule, AtRuleBlockKind::GroupRuleList),
80        "@scope" => (SyntaxKind::ScopeRule, AtRuleBlockKind::GroupRuleList),
81        "@starting-style" => (
82            SyntaxKind::StartingStyleRule,
83            AtRuleBlockKind::GroupRuleList,
84        ),
85        "@nest" => (SyntaxKind::NestRule, AtRuleBlockKind::DeclarationList),
86        "@keyframes" => (SyntaxKind::KeyframesRule, AtRuleBlockKind::Keyframes),
87        "@font-face" => (SyntaxKind::FontFaceRule, AtRuleBlockKind::DeclarationList),
88        "@page" => (SyntaxKind::PageRule, AtRuleBlockKind::DeclarationList),
89        "@property" => (SyntaxKind::PropertyRule, AtRuleBlockKind::DeclarationList),
90        "@function" => (SyntaxKind::FunctionRule, AtRuleBlockKind::DeclarationList),
91        "@counter-style" => (
92            SyntaxKind::CounterStyleRule,
93            AtRuleBlockKind::DeclarationList,
94        ),
95        "@font-palette-values" => (
96            SyntaxKind::FontPaletteValuesRule,
97            AtRuleBlockKind::DeclarationList,
98        ),
99        "@color-profile" => (
100            SyntaxKind::ColorProfileRule,
101            AtRuleBlockKind::DeclarationList,
102        ),
103        "@position-try" => (
104            SyntaxKind::PositionTryRule,
105            AtRuleBlockKind::DeclarationList,
106        ),
107        "@font-feature-values" => (
108            SyntaxKind::FontFeatureValuesRule,
109            AtRuleBlockKind::GroupRuleList,
110        ),
111        "@stylistic" => (
112            SyntaxKind::FontFeatureValuesStylisticRule,
113            AtRuleBlockKind::DeclarationList,
114        ),
115        "@styleset" => (
116            SyntaxKind::FontFeatureValuesStylesetRule,
117            AtRuleBlockKind::DeclarationList,
118        ),
119        "@character-variant" => (
120            SyntaxKind::FontFeatureValuesCharacterVariantRule,
121            AtRuleBlockKind::DeclarationList,
122        ),
123        "@swash" => (
124            SyntaxKind::FontFeatureValuesSwashRule,
125            AtRuleBlockKind::DeclarationList,
126        ),
127        "@ornaments" => (
128            SyntaxKind::FontFeatureValuesOrnamentsRule,
129            AtRuleBlockKind::DeclarationList,
130        ),
131        "@annotation" => (
132            SyntaxKind::FontFeatureValuesAnnotationRule,
133            AtRuleBlockKind::DeclarationList,
134        ),
135        "@historical-forms" => (
136            SyntaxKind::FontFeatureValuesHistoricalFormsRule,
137            AtRuleBlockKind::DeclarationList,
138        ),
139        "@view-transition" => (
140            SyntaxKind::ViewTransitionRule,
141            AtRuleBlockKind::DeclarationList,
142        ),
143        "@charset" => (SyntaxKind::CharsetRule, AtRuleBlockKind::Raw),
144        "@import" => (SyntaxKind::ImportRule, AtRuleBlockKind::Raw),
145        "@namespace" => (SyntaxKind::NamespaceRule, AtRuleBlockKind::Raw),
146        "@custom-media" => (SyntaxKind::CustomMediaRule, AtRuleBlockKind::Raw),
147        text if is_page_margin_at_rule(text) => {
148            (SyntaxKind::PageMarginRule, AtRuleBlockKind::DeclarationList)
149        }
150        _ => return None,
151    };
152    Some(AtRuleSpec {
153        node_kind,
154        block_kind,
155    })
156}
157
158fn at_rule_is_vendor_prefixed_keyframes(text: &str) -> bool {
159    let Some(rest) = text.strip_prefix("@-") else {
160        return false;
161    };
162    let Some((vendor, rule)) = rest.split_once('-') else {
163        return false;
164    };
165    !vendor.is_empty() && rule == "keyframes"
166}
167
168pub(crate) fn scss_at_rule_spec(text: &str) -> Option<AtRuleSpec> {
169    let (node_kind, block_kind) = match text {
170        "@use" => (SyntaxKind::ScssUseRule, AtRuleBlockKind::Raw),
171        "@forward" => (SyntaxKind::ScssForwardRule, AtRuleBlockKind::Raw),
172        "@mixin" => (
173            SyntaxKind::ScssMixinDeclaration,
174            AtRuleBlockKind::DeclarationList,
175        ),
176        "@include" => (
177            SyntaxKind::ScssIncludeRule,
178            AtRuleBlockKind::DeclarationList,
179        ),
180        "@function" => (
181            SyntaxKind::ScssFunctionDeclaration,
182            AtRuleBlockKind::DeclarationList,
183        ),
184        "@return" => (SyntaxKind::ScssReturnRule, AtRuleBlockKind::Raw),
185        "@extend" => (SyntaxKind::ScssExtendRule, AtRuleBlockKind::Raw),
186        "@if" => (SyntaxKind::ScssControlIf, AtRuleBlockKind::DeclarationList),
187        "@else" => (
188            SyntaxKind::ScssControlElse,
189            AtRuleBlockKind::DeclarationList,
190        ),
191        "@each" => (
192            SyntaxKind::ScssControlEach,
193            AtRuleBlockKind::DeclarationList,
194        ),
195        "@for" => (SyntaxKind::ScssControlFor, AtRuleBlockKind::DeclarationList),
196        "@while" => (
197            SyntaxKind::ScssControlWhile,
198            AtRuleBlockKind::DeclarationList,
199        ),
200        "@at-root" => (SyntaxKind::ScssAtRootRule, AtRuleBlockKind::DeclarationList),
201        "@error" => (SyntaxKind::ScssErrorRule, AtRuleBlockKind::Raw),
202        "@warn" => (SyntaxKind::ScssWarnRule, AtRuleBlockKind::Raw),
203        "@debug" => (SyntaxKind::ScssDebugRule, AtRuleBlockKind::Raw),
204        "@content" => (SyntaxKind::ScssContentRule, AtRuleBlockKind::Raw),
205        _ => return None,
206    };
207    Some(AtRuleSpec {
208        node_kind,
209        block_kind,
210    })
211}
212
213fn is_page_margin_at_rule(text: &str) -> bool {
214    matches!(
215        text,
216        "@top-left-corner"
217            | "@top-left"
218            | "@top-center"
219            | "@top-right"
220            | "@top-right-corner"
221            | "@bottom-left-corner"
222            | "@bottom-left"
223            | "@bottom-center"
224            | "@bottom-right"
225            | "@bottom-right-corner"
226            | "@left-top"
227            | "@left-middle"
228            | "@left-bottom"
229            | "@right-top"
230            | "@right-middle"
231            | "@right-bottom"
232    )
233}