├── .gitignore ├── LICENSE ├── README.md ├── css ├── ast │ ├── ast.v │ ├── table.v │ └── types.v ├── checker │ ├── checker.v │ ├── constants.v │ ├── functions.v │ ├── properties.v │ ├── rule.v │ └── tests │ │ ├── fn_gradient.css │ │ ├── fn_gradient.out │ │ ├── fn_rgb.css │ │ ├── fn_rgb.out │ │ ├── fn_url.css │ │ ├── fn_url.out │ │ ├── prop_alpha_value.css │ │ ├── prop_alpha_value.out │ │ ├── prop_border.css │ │ ├── prop_border.out │ │ ├── prop_color.css │ │ ├── prop_color.out │ │ ├── prop_endings_color.css │ │ ├── prop_endings_color.out │ │ ├── prop_flex.css │ │ ├── prop_flex.out │ │ ├── prop_font.css │ │ ├── prop_font.out │ │ ├── prop_gap.css │ │ ├── prop_gap.out │ │ ├── prop_margin_padding.css │ │ ├── prop_margin_padding.out │ │ ├── prop_overflow.css │ │ ├── prop_overflow.out │ │ ├── prop_shadow.css │ │ ├── prop_shadow.out │ │ ├── prop_single_dim.css │ │ ├── prop_single_dim.out │ │ ├── prop_starts_single_dim.css │ │ ├── prop_starts_single_dim.out │ │ ├── prop_text.css │ │ ├── prop_text.out │ │ ├── test_program.v │ │ ├── unsupported_property.css │ │ └── unsupported_property.out ├── datatypes │ ├── README.md │ ├── datatypes.v │ ├── functions.v │ └── tests │ │ └── color_test.v ├── errors │ └── errors.v ├── functions.v ├── gen │ ├── README.md │ ├── block.v │ ├── gen.v │ ├── rules.v │ ├── selector.v │ └── tests │ │ ├── gen_test.v │ │ └── testdata │ │ ├── pretty.css │ │ ├── pretty_out.css │ │ └── pretty_out.min.css ├── grouped.v ├── lexer │ ├── lexer.v │ └── util.v ├── parser │ ├── at_rules.v │ ├── declaration.v │ ├── parser.v │ ├── parser_bootstrap_test.v │ ├── selector.v │ └── tests │ │ ├── attr_selector.css │ │ ├── attr_selector.out │ │ ├── attr_selector_strict.css │ │ ├── attr_selector_strict.out │ │ ├── charset.css │ │ ├── charset.out │ │ ├── continue_on_single_missing_parentheses.css │ │ ├── continue_on_single_missing_parentheses.out │ │ ├── declaration.css │ │ ├── declaration.out │ │ ├── empty_at_rule.css │ │ ├── empty_at_rule.out │ │ ├── empty_declaration.css │ │ ├── empty_declaration.out │ │ ├── empty_selector.css │ │ ├── empty_selector.out │ │ ├── hex_colors.css │ │ ├── hex_colors.out │ │ ├── important.css │ │ ├── important.out │ │ ├── keyframes.css │ │ ├── keyframes.out │ │ ├── last_declaration_without_semi.css │ │ ├── last_declaration_without_semi.out │ │ ├── last_declaration_without_semi_strict.css │ │ ├── last_declaration_without_semi_strict.out │ │ ├── layer.css │ │ ├── layer.out │ │ ├── media.css │ │ ├── media.out │ │ ├── media_feature.css │ │ ├── media_feature.out │ │ ├── missing_selector_ident.css │ │ ├── missing_selector_ident.out │ │ ├── pseudo_selector.css │ │ ├── pseudo_selector.out │ │ ├── raw_url.css │ │ ├── raw_url.out │ │ ├── test_program.v │ │ ├── unkown_at_rule.css │ │ └── unkown_at_rule.out ├── pref │ └── pref.v ├── properties.v ├── tests │ ├── compiler_errors_test.v │ ├── functions │ │ ├── gradient_test.v │ │ ├── rgb_test.v │ │ └── url_test.v │ ├── properties │ │ ├── alpha_test.v │ │ ├── background_test.v │ │ ├── border_test.v │ │ ├── browser_prefix_test.v │ │ ├── color_test.v │ │ ├── flex_test.v │ │ ├── font_test.v │ │ ├── margin_padding_test.v │ │ ├── overflow_test.v │ │ ├── shadow_test.v │ │ └── single_dimension_test.v │ ├── selector_test.v │ └── specificity_test.v ├── token │ ├── pos.v │ └── token.v ├── types.v └── util │ └── util.v ├── examples ├── simple.css └── simple.v ├── progress └── parser.md ├── tests ├── bootstrap_test.v └── testdata │ ├── all_properties.css │ ├── bootstrap.css │ ├── bootstrap.min.css │ ├── order.css │ ├── simple.css │ └── tokens.css └── v.mod /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *.txt 3 | js/ 4 | dist/ 5 | test_stuff/ 6 | 7 | # don't include top level V files 8 | /*.v -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Casper Küthe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | MIT License 24 | 25 | Copyright (c) 2019-2023 Alexander Medvednikov 26 | 27 | Permission is hereby granted, free of charge, to any person obtaining a copy 28 | of this software and associated documentation files (the "Software"), to deal 29 | in the Software without restriction, including without limitation the rights 30 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 31 | copies of the Software, and to permit persons to whom the Software is 32 | furnished to do so, subject to the following conditions: 33 | 34 | The above copyright notice and this permission notice shall be included in all 35 | copies or substantial portions of the Software. 36 | 37 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 38 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 39 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 40 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 41 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 42 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 43 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CSS Parser 2 | > **Note:** 3 | > This CSS parser is not suited for production yet and the API will change in the future! 4 | 5 | 6 | The aim of this module is to serve as an intermediate stage in graphics rendering 7 | by supporting CSS as styling language. 8 | 9 | It parses a CSS file and returns a list of rules. These rules are sorted by their specificity, 10 | so the programmer only needs to implement a way to render the rules. 11 | 12 | **Acknowledgement:** 13 | A lot of inpsiration (and some code) is taken from the V compiler. 14 | 15 | ## Installation 16 | 17 | Install via the V package manager: 18 | ```bash 19 | v install Casper64.css 20 | ``` 21 | 22 | Or add `Casper64.css` to the depencies array in your `v.mod` file and run `v install`. 23 | 24 | ## Features 25 | 26 | - 100% Written in V 27 | - Parser can parse `bootstrap.min.css` without errors 28 | - Parser and checker produce errors and warnings 29 | - Rules are sorted by CSS Specificity 30 | - Property validator (checker stage) can be embedded to handle custom properties 31 | 32 | ### TODO 33 | 34 | - [ ] Implement more properties, functions and CSS datatypes. 35 | - [ ] CSS Animations, `@keyframe` rules 36 | - [ ] CSS Variables, probably will have to rely on a combination using the DOM 37 | - [ ] Optimizations: store rules in a B+ tree by specificity, merge styles etc. 38 | - [ ] Minifier 39 | 40 | ## Project structure 41 | 42 | The `css` folder follows a similair directory structure as the V compiler. 43 | 44 | The compiler stage is as follows: `lexer` produces tokens -> `parser` produces AST -> `checker` 45 | validates the AST and transforms it to usable structs and sumtypes. 46 | 47 | ## Usage Example 48 | 49 | Run the example: 50 | ```v 51 | v run examples/simple.v 52 | ``` 53 | 54 | If you are confused why the resulting color is `red` please referer to 55 | [CSS specificity](https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity). 56 | 57 | The [properties](css/tests/properties/) folder also contains examples on how properties can be used. 58 | 59 | ## Tests 60 | 61 | Run all tests with `v test .`, the tests in `css/gen` should fail. 62 | 63 | ### Runnining individual checks 64 | ```bash 65 | v run css/parser/tests/test_program.v css/parser/tests/file.css 66 | ``` 67 | You can swap out 'parser' for 'checker' 68 | 69 | ## API 70 | 71 | ### Properties 72 | 73 | A list of properties and their types. Some property names start with `-`, this means 74 | that all CSS properties with this ending have the same type. 75 | 76 | See [types.v](css/types.v) and [properties.v](css/properties.v) to see 77 | the exact specification of each struct/sumtype. 78 | 79 | | Type | Properties | Remarks | 80 | | --- | --- | --- | 81 | | `AlphaValue`| 'opacity' | | 82 | | `Background`| 'background' | struct containing all `background-` properties | 83 | | `Border` | 'border' | struct containing all `border-` properties | 84 | | `BorderColors` | 'border-color' | struct containing all 4 colors for each border side, will be merged into `Border` | 85 | | `BorderRadius` | 'border-radius' | struct containing the radius for each corner, will be merged into `Border` | 86 | | `BorderStyles` | 'border-style' | struct containing all styles for each border, will be merged into `Border` | 87 | | `ColorValue` | 'accent-color', 'background-color', 'caret-color', 'color', 'column-rule-color', 'outline-color' | A sumtype representing a color value, each property ending with `-color` will be this type. | 88 | | `DimensionValue` | 'block-size', 'bottom', 'column-gap', 'flex-basis', 'height', 'inline-size', 'left','letter-spacing' 'line-height', 'max-height', 'max-width', 'min-height', 'min-width', 'order', 'orphans', 'perspective', 'right', 'row-gap', 'tab-size', 'text-indent', 'top', 'vertical-align', 'widows', 'width', 'word-spacing', 'z-index' | A sumtype representing properties/values that represent a dimension/length, e.g. `10px`, or `50%`. | 89 | | `Font` | 'font', 'font-family', 'font-size', 'font-stretch', 'font-weight' | struct containing all `font-` properties | 90 | | `FontFamily` | 'font-family' | will be merged into 'font' | 91 | | `FontStretch` | 'font-stretch' | a sumtype containing the possible font-stretch properties, will be merged into 'font' | 92 | | `FontWeight` | 'font-weight' | will be merged into 'font' | 93 | | `FlexBox` | 'flex' | struct containg all `flex-` properties. | 94 | | `FlexDirection` | 'flex-direction' | will be merged into 'flex' | 95 | | `FlexSize` | 'flex-grow', 'flex-shrink' | will be merged into 'flex' | 96 | | `FlexWrap` | 'flex-wrap' | A sumtype containg the possible flex-wrap types, will be merged into 'flex' | 97 | | `FourDimensions` | 'padding', 'margin', 'border-width' | A struct containing 4 `DimensionValue` fields for top, right, bottom and left | 98 | | `Gap` | 'gap' | A struct containing 'row-gap' and 'column-gap' | 99 | | `Image` | 'background-image' | A sumtype holding the different values for the `image` CSS datatype | 100 | | `Keyword` | 'align-content', 'align-items', 'align-self', 'all', 'appearance', 'backface-visibility', 'border-collapse', 'box-sizing', 'caption-side', 'clear', 'cursor', 'direction', 'display', 'empty-cells', 'float', 'forced-color-adjust', 'isolation', 'justify-content', 'justify-items', 'justify-self', 'mix-blend-mode', 'object-fit', 'overflow-x', 'overflow-y', 'pointer-events', 'position', 'print-color-adjust', 'resize', 'scroll-behavior', 'table-layout', 'text-align', 'text-align-last', 'text-justify', 'text-rendering', 'text-transform', 'text-wrap', 'touch-action', 'unicode-bidi', 'user-select', 'visibility', 'white-space', 'word-break', 'word-wrap', 'writing-mode' | A type alias for string representing a CSS keyword like `inherit` | 101 | | `Overflow` | 'overflow' | A struct containing the properties 'overflow-x' and 'overflow-y' | 102 | | `ShadowValue` | 'box-shadow', 'text-shadow' | A struct containing all properties for a CSS shadow | 103 | | `Text` | 'text' | A struct containing all text CSS properties, starting with `text-` | 104 | | `TextCombineUpright` | 'text-combine-upright' | See MDN reference [here](https://developer.mozilla.org/en-US/docs/Web/CSS/text-combine-upright) section about `digits`. Will be merged into `Text` | 105 | | `TextOverflow` | 'text-overflow' | Sumtype of `Keyword` and a `string`, the string type will represent the ellipsis text specified by the user, will be merged into `Text` | 106 | 107 | ### Functions 108 | 109 | See [functions.v](css/functions.v) for the exact specifications. 110 | 111 | | Type | Function | Remarks | 112 | | --- | --- | --- | 113 | | `Gradient` | 'linear-gradient', 'radial-gradient', 'repeating-linear-gradient', 'repeating-radial-gradient' | Used as datatype for each property that can have a gradient like 'background-image' | 114 | | `Url` | 'url', 'src' | The type of url is defined in the `UrlKind` enum | 115 | 116 | ### Datatypes 117 | 118 | See [datatypes.v](css/datatypes//datatypes.v) for a representation of currently supported CSS Datatypes. 119 | -------------------------------------------------------------------------------- /css/ast/ast.v: -------------------------------------------------------------------------------- 1 | module ast 2 | 3 | // implementing ast from https://github.com/csstree/csstree/blob/master/docs/ast.md 4 | // following the mdn specs for css https://developer.mozilla.org/en-US/docs/Web/CSS 5 | import css.token 6 | 7 | // empty struct 8 | pub struct Empty {} 9 | 10 | // a css file 11 | @[heap] 12 | pub struct StyleSheet { 13 | pub: 14 | file_path string 15 | pub mut: 16 | imports []ImportRule 17 | rules []RuleType 18 | } 19 | 20 | pub struct Rule { 21 | pub: 22 | pos token.Pos 23 | pub mut: 24 | prelude Prelude 25 | block Block 26 | } 27 | 28 | pub struct AtRule { 29 | pub: 30 | pos token.Pos 31 | pub mut: 32 | typ AtType 33 | prelude []Node 34 | children []Node 35 | } 36 | 37 | pub struct ImportRule { 38 | pub: 39 | pos token.Pos 40 | pub mut: 41 | typ ImportType 42 | path string 43 | layer ?string 44 | // supports []SupportRule 45 | media_query_list ?MediaQueryList 46 | } 47 | 48 | pub struct KeyframesRule { 49 | pub: 50 | pos token.Pos 51 | pub mut: 52 | percentage string 53 | declarations []Node 54 | } 55 | 56 | pub struct Block { 57 | pub: 58 | pos token.Pos 59 | pub mut: 60 | declarations []Node 61 | } 62 | 63 | pub struct Declaration { 64 | pub: 65 | pos token.Pos 66 | pub mut: 67 | property string 68 | important bool 69 | value Value 70 | } 71 | 72 | pub struct Function { 73 | pub: 74 | pos token.Pos 75 | pub mut: 76 | name string 77 | children []Node 78 | } 79 | 80 | pub struct Dimension { 81 | pub: 82 | pos token.Pos 83 | pub mut: 84 | value string 85 | unit string 86 | } 87 | 88 | pub struct Hash { 89 | pub: 90 | pos token.Pos 91 | pub mut: 92 | value string 93 | } 94 | 95 | pub struct Ident { 96 | pub: 97 | pos token.Pos 98 | pub mut: 99 | name string 100 | } 101 | 102 | pub struct Number { 103 | pub: 104 | pos token.Pos 105 | pub mut: 106 | value string 107 | } 108 | 109 | pub struct Operator { 110 | pub: 111 | pos token.Pos 112 | pub mut: 113 | kind OperatorKind 114 | } 115 | 116 | pub fn (op Operator) == (other Operator) bool { 117 | return op.kind == other.kind 118 | } 119 | 120 | pub struct Parentheses { 121 | pub: 122 | pos token.Pos 123 | pub mut: 124 | children []Node 125 | } 126 | 127 | pub struct MediaFeature { 128 | pub: 129 | pos token.Pos 130 | pub mut: 131 | name string 132 | value ?Node 133 | } 134 | 135 | pub struct MediaQuery { 136 | pub: 137 | pos token.Pos 138 | pub mut: 139 | children []Node 140 | } 141 | 142 | pub struct MediaQueryList { 143 | pub: 144 | pos token.Pos 145 | pub mut: 146 | children []MediaQuery 147 | } 148 | 149 | // Raw is used for some functions like `url()` or anything that can't be properly parsed 150 | pub struct Raw { 151 | pub: 152 | pos token.Pos 153 | pub mut: 154 | value string 155 | } 156 | 157 | pub struct Value { 158 | pub: 159 | pos token.Pos 160 | pub mut: 161 | children []Node 162 | } 163 | 164 | pub struct SelectorList { 165 | pub: 166 | pos token.Pos 167 | pub mut: 168 | children []Node 169 | } 170 | 171 | pub struct Selector { 172 | pub: 173 | pos token.Pos 174 | pub mut: 175 | children []Node 176 | } 177 | 178 | // matches tests if the selectors in `s1` match the selectors in `nodes`: 179 | // it checks if s1 is a subset of nodes 180 | // the selectors children will be split on combinators and onl the last part is matched: 181 | // .test > .other#id will only try to match for .other#id 182 | pub fn (selector Selector) matches(other_selectors []Node) bool { 183 | mut last_nodes := []Node{} 184 | for n in selector.children { 185 | if n is Combinator { 186 | last_nodes.clear() 187 | } else { 188 | last_nodes << n 189 | } 190 | } 191 | 192 | for sub_s1 in last_nodes { 193 | if sub_s1 !in other_selectors { 194 | return false 195 | } 196 | // match sub_s1 { 197 | // IdSelector, ClassSelector, TypeSelector { 198 | // if sub_s1 !in s2.children { 199 | // return false 200 | // } 201 | // } 202 | // else { 203 | // return false 204 | // } 205 | // } 206 | } 207 | return true 208 | } 209 | 210 | pub struct ClassSelector { 211 | pub: 212 | pos token.Pos 213 | pub mut: 214 | name string 215 | } 216 | 217 | pub struct TypeSelector { 218 | pub: 219 | pos token.Pos 220 | pub mut: 221 | name string 222 | } 223 | 224 | pub struct IdSelector { 225 | pub: 226 | pos token.Pos 227 | pub mut: 228 | name string 229 | } 230 | 231 | pub struct Combinator { 232 | pub: 233 | pos token.Pos 234 | pub mut: 235 | kind string 236 | } 237 | 238 | pub struct PseudoClassSelector { 239 | pub: 240 | pos token.Pos 241 | pub mut: 242 | name string 243 | children []Node 244 | } 245 | 246 | pub struct PseudoElementSelector { 247 | pub: 248 | pos token.Pos 249 | pub mut: 250 | name string 251 | children []Node 252 | } 253 | 254 | pub struct AttributeSelector { 255 | pub: 256 | pos token.Pos 257 | pub mut: 258 | name Ident 259 | matcher AttributeMatchType 260 | value ?String 261 | } 262 | 263 | pub struct String { 264 | pub: 265 | pos token.Pos 266 | pub mut: 267 | value string 268 | } 269 | 270 | pub type Node = AtRule 271 | | AttributeSelector 272 | | Block 273 | | ClassSelector 274 | | Combinator 275 | | Declaration 276 | | Dimension 277 | | Empty 278 | | Function 279 | | Hash 280 | | IdSelector 281 | | Ident 282 | | ImportRule 283 | | KeyframesRule 284 | | MediaFeature 285 | | MediaQuery 286 | | MediaQueryList 287 | | NodeError 288 | | Number 289 | | Operator 290 | | Parentheses 291 | | Prelude 292 | | PseudoClassSelector 293 | | PseudoElementSelector 294 | | PseudoSelector 295 | | Raw 296 | | Rule 297 | | RuleType 298 | | Selector 299 | | SelectorList 300 | | String 301 | | StyleSheet 302 | | TypeSelector 303 | | Value 304 | 305 | pub fn (node Node) children() []Node { 306 | return match node { 307 | AtRule { 308 | mut children := []Node{} 309 | children << node.prelude 310 | children << node.children 311 | children 312 | } 313 | Rule { 314 | mut children := []Node{} 315 | children << Node(node.prelude) 316 | children << node.block 317 | children 318 | } 319 | ImportRule { 320 | if l := node.media_query_list { 321 | [Node(l)] 322 | } else { 323 | []Node{} 324 | } 325 | } 326 | Block, KeyframesRule { 327 | node.declarations 328 | } 329 | Declaration { 330 | [Node(node.value)] 331 | } 332 | Function, Parentheses, MediaQuery, Value, SelectorList, Selector, PseudoClassSelector, 333 | PseudoElementSelector { 334 | node.children 335 | } 336 | MediaFeature { 337 | if f := node.value { 338 | return [f] 339 | } else { 340 | []Node{} 341 | } 342 | } 343 | MediaQueryList { 344 | mut children := []Node{} 345 | children << node.children 346 | children 347 | } 348 | else { 349 | []Node{} 350 | } 351 | } 352 | } 353 | 354 | pub fn (node Node) pos() token.Pos { 355 | return match node { 356 | StyleSheet, Empty { 357 | token.Pos{} 358 | } 359 | Rule, AtRule, ImportRule, KeyframesRule, Block, Declaration, Function, Dimension, Hash, 360 | Ident, Number, Operator, Parentheses, MediaFeature, MediaQuery, MediaQueryList, Raw, Value, 361 | SelectorList, Selector, ClassSelector, TypeSelector, IdSelector, Combinator, 362 | PseudoClassSelector, PseudoElementSelector, AttributeSelector, String { 363 | node.pos 364 | } 365 | else { 366 | token.Pos{} 367 | } 368 | } 369 | } 370 | -------------------------------------------------------------------------------- /css/ast/table.v: -------------------------------------------------------------------------------- 1 | module ast 2 | 3 | pub struct RuleNode { 4 | pub: 5 | selector Selector 6 | specificity Specificity 7 | pub mut: 8 | declaration_idx int 9 | } 10 | 11 | @[heap; minify] 12 | pub struct Table { 13 | pub mut: 14 | // front = least specific, back = most specific 15 | rules []RuleNode 16 | raw_declarations [][]Node 17 | // at_rules []AtRuleNode 18 | } 19 | 20 | pub fn (mut t Table) insert_rule(selector_list []Node, declarations []Node) { 21 | for selector in selector_list { 22 | if selector is Selector { 23 | specificity := Specificity.from_selectors(selector.children) 24 | node := RuleNode{ 25 | selector: selector 26 | specificity: specificity 27 | declaration_idx: t.raw_declarations.len 28 | } 29 | 30 | t.rules << node 31 | } 32 | } 33 | 34 | t.raw_declarations << [declarations] 35 | } 36 | 37 | pub fn (mut t Table) sort_rules() { 38 | t.rules.sort(|a, b| a.specificity <= b.specificity) 39 | } 40 | 41 | // Specificity represents CSS specificity 42 | // https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity 43 | pub struct Specificity { 44 | pub mut: 45 | col1 int 46 | col2 int 47 | col3 int 48 | } 49 | 50 | pub fn (s &Specificity) str() string { 51 | return '${s.col1}-${s.col2}-${s.col3}' 52 | } 53 | 54 | pub fn Specificity.from_selectors(selectors []Node) Specificity { 55 | mut s := Specificity{} 56 | 57 | for current_selector in selectors { 58 | match current_selector { 59 | IdSelector { 60 | s.col1++ 61 | } 62 | AttributeSelector, ClassSelector { 63 | s.col2++ 64 | } 65 | PseudoClassSelector { 66 | if current_selector.name == 'where' { 67 | // :where doesn't count for specificity, same applies for its children 68 | continue 69 | } else if current_selector.name !in ['is', 'has', 'not'] { 70 | // :is, :has and :not don't count for specificity, but their children do 71 | s.col2++ 72 | } 73 | s += Specificity.from_selectors(current_selector.children) 74 | } 75 | TypeSelector { 76 | if current_selector.name != '*' { 77 | // the universal selector does not count for specificity 78 | s.col3++ 79 | } 80 | } 81 | PseudoElementSelector { 82 | s.col3++ 83 | s += Specificity.from_selectors(current_selector.children) 84 | } 85 | else {} 86 | } 87 | } 88 | 89 | return s 90 | } 91 | 92 | fn (a Specificity) == (b Specificity) bool { 93 | return a.col1 == b.col1 && a.col2 == b.col2 && a.col3 == b.col3 94 | } 95 | 96 | fn (a Specificity) < (b Specificity) bool { 97 | if a.col1 < b.col1 { 98 | return true 99 | } else if a.col1 == b.col1 { 100 | if a.col2 < b.col2 { 101 | return true 102 | } else if a.col2 == b.col2 { 103 | return a.col3 < b.col3 104 | } 105 | } 106 | return false 107 | } 108 | 109 | fn (a Specificity) + (b Specificity) Specificity { 110 | return Specificity{ 111 | col1: a.col1 + b.col1 112 | col2: a.col2 + b.col2 113 | col3: a.col3 + b.col3 114 | } 115 | } 116 | 117 | fn (a Specificity) - (b Specificity) Specificity { 118 | return Specificity{ 119 | col1: a.col1 - b.col1 120 | col2: a.col2 - b.col2 121 | col3: a.col3 - b.col3 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /css/ast/types.v: -------------------------------------------------------------------------------- 1 | module ast 2 | 3 | import css.token 4 | 5 | pub type RuleType = AtRule | Empty | KeyframesRule | Rule 6 | 7 | pub type Prelude = Raw | SelectorList 8 | 9 | pub enum AtType { 10 | unkown 11 | charset 12 | container 13 | counter_style 14 | font_face 15 | @import 16 | keyframes 17 | layer 18 | media 19 | } 20 | 21 | pub struct NodeError { 22 | pub: 23 | pos token.Pos 24 | msg string 25 | code int 26 | is_unexpected bool 27 | } 28 | 29 | pub fn (n NodeError) msg() string { 30 | return n.msg 31 | } 32 | 33 | pub fn (n NodeError) code() int { 34 | return n.code 35 | } 36 | 37 | pub type PseudoSelector = PseudoClassSelector | PseudoElementSelector | Raw 38 | 39 | pub enum AttributeMatchType { 40 | @none 41 | exact // = 42 | contains // *= 43 | starts_with // ^= 44 | ends_with // $= 45 | } 46 | 47 | pub enum OperatorKind { 48 | plus // + 49 | min // - 50 | mul // * 51 | div // / 52 | comma // , 53 | } 54 | 55 | pub enum ImportType { 56 | url 57 | path 58 | } 59 | -------------------------------------------------------------------------------- /css/checker/checker.v: -------------------------------------------------------------------------------- 1 | module checker 2 | 3 | import css 4 | import css.ast 5 | import css.pref 6 | import css.errors 7 | import css.token 8 | 9 | // Validator is an interface which you can extend to check any custom properties 10 | pub interface Validator { 11 | mut: 12 | validate_property(string, ast.Value) !css.Value 13 | } 14 | 15 | // seperating the property validator from the checker means that the validator 16 | // can be extended by embedding it allowing custom properties and the ability 17 | // to override default behaviour of css values. 18 | // You can make an instance of Property by calling the `make_property_validator` 19 | // on an instance of `Checker` 20 | @[noinit] 21 | pub struct PropertyValidator { 22 | get_details fn () string = unsafe { nil } 23 | warn_with_pos fn (string, token.Pos) = unsafe { nil } 24 | error_with_pos fn (string, token.Pos) ast.NodeError = unsafe { nil } 25 | pub mut: 26 | variables map[string]ast.Value 27 | } 28 | 29 | pub fn (pv &PropertyValidator) unsupported_property(property string) string { 30 | return 'unsupported property "${property}"! Check the "CAN_I_USE.md" for a list of supported properties' 31 | } 32 | 33 | pub fn validate(tree &ast.StyleSheet, mut table ast.Table, prefs pref.Preferences) ![]css.Rule { 34 | mut checker := &Checker{ 35 | file_path: tree.file_path 36 | prefs: prefs 37 | table: table 38 | rules: []css.Rule{cap: tree.rules.len} 39 | // tmp_declarations: []map[string]css.RawValue{len: table.rules.len} 40 | } 41 | 42 | // table.sort_rules() 43 | 44 | checker.validator = checker.make_property_validator() 45 | 46 | checker.validate(tree) 47 | if checker.has_errored { 48 | return error('checker has returned with errors!') 49 | } 50 | 51 | checker.sort_rules() 52 | return checker.rules 53 | } 54 | 55 | @[heap; minify] 56 | pub struct Checker { 57 | prefs pref.Preferences 58 | file_path string 59 | mut: 60 | error_details []string 61 | validator Validator @[noinit] 62 | // tmp_declarations []map[string]css.RawValue 63 | pub mut: 64 | table &ast.Table = unsafe { nil } 65 | has_errored bool 66 | rules []css.Rule 67 | } 68 | 69 | // make_property_validator returns a new `PropertyValidator` instance and sets the error functions 70 | pub fn (c Checker) make_property_validator() PropertyValidator { 71 | return PropertyValidator{ 72 | get_details: c.get_details 73 | warn_with_pos: c.warn_with_pos 74 | error_with_pos: c.error_with_pos 75 | } 76 | } 77 | 78 | pub fn (mut c Checker) sort_rules() { 79 | c.rules.sort(|a, b| a.specificity <= b.specificity) 80 | } 81 | 82 | pub fn (mut c Checker) validate(tree &ast.StyleSheet) { 83 | // TODO: first sort rules by selector so the CSS variables can be replaced correctly, maybe do this in the parser?? 84 | for rule in tree.rules { 85 | match rule { 86 | ast.Rule { 87 | c.validate_rule(rule) or { 88 | if err is ast.NodeError { 89 | c.error_with_pos(err.msg, err.pos) 90 | } else { 91 | c.error(err.msg()) 92 | } 93 | continue 94 | } 95 | } 96 | ast.KeyframesRule {} 97 | else {} 98 | } 99 | } 100 | // TODO: @keyframes rule 101 | // for rule_node in c.table.rules { 102 | // if rule := c.validate_rule(rule_node) { 103 | // c.rules << rule 104 | // } else { 105 | // if err is ast.NodeError { 106 | // c.error_with_pos(err.msg, err.pos) 107 | // } else { 108 | // c.error(err.msg()) 109 | // } 110 | // } 111 | // } 112 | } 113 | 114 | pub fn (mut c Checker) get_details() string { 115 | mut details := '' 116 | if c.error_details.len > 0 { 117 | details = '\n' + c.error_details.join('\n') 118 | c.error_details = [] 119 | } 120 | return details 121 | } 122 | 123 | pub fn (mut c Checker) warn_with_pos(msg string, pos token.Pos) { 124 | details := c.get_details() 125 | if !c.prefs.suppress_output { 126 | errors.show_compiler_message('warning:', 127 | msg: msg 128 | details: details 129 | file_path: c.file_path 130 | pos: pos 131 | ) 132 | } 133 | } 134 | 135 | pub fn (mut c Checker) error(msg string) ast.NodeError { 136 | c.has_errored = true 137 | details := c.get_details() 138 | 139 | // TODO: better handle normal errors 140 | eprintln(msg) 141 | eprintln('details: ${details}') 142 | 143 | return ast.NodeError{ 144 | msg: msg 145 | } 146 | } 147 | 148 | pub fn (mut c Checker) error_with_pos(msg string, pos token.Pos) ast.NodeError { 149 | c.has_errored = true 150 | details := c.get_details() 151 | 152 | if !c.prefs.suppress_output { 153 | errors.show_compiler_message('error:', 154 | msg: msg 155 | details: details 156 | file_path: c.file_path 157 | pos: pos 158 | ) 159 | } 160 | 161 | return ast.NodeError{ 162 | msg: msg 163 | pos: pos 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /css/checker/constants.v: -------------------------------------------------------------------------------- 1 | module checker 2 | 3 | const four_dim_endings = ['-left', '-top', 'right', '-bottom'] 4 | const gradient_directions = ['left', 'top', 'right', 'bottom'] 5 | const pseudo_class_selectors_functions = ['current', 'dir', 'has', 'is', 'lang', 'is', 'not', 6 | 'nth-child', 'nth-of-type', 'nth-last-child', 'nth-of-type', 'where'] 7 | const pseudo_element_selectors_functions = ['part', 'slotted'] 8 | const valid_color_functions = ['rgb', 'rgba', 'hsl'] 9 | const valid_image_fns = ['url', 'linear-gradient', 'radial-gradient', 'repeating-linear-gradient', 10 | 'repeating-radial-gradient'] 11 | const valid_units = ['em', 'rem', 'px', 'vw', 'vh'] 12 | const vendor_prefixes = ['-moz-', '-webkit-', '-ms-', '-o-'] 13 | const valid_gradient_functions = ['linear-gradient', 'radial-gradient', 'repeating-linear-gradient', 14 | 'repeating-radial-gradient'] 15 | -------------------------------------------------------------------------------- /css/checker/functions.v: -------------------------------------------------------------------------------- 1 | module checker 2 | 3 | import css 4 | import css.ast 5 | import css.datatypes 6 | import css.errors 7 | 8 | // `rgb()`, `rgba()` 9 | pub fn (pv &PropertyValidator) validate_fn_rgb(func ast.Function) !datatypes.Color { 10 | mut rgbas := []u8{} 11 | 12 | for i, node in func.children { 13 | match node { 14 | ast.Operator { 15 | // `/ 80%` alpha syntax 16 | if node.kind == .div { 17 | if i + 1 == func.children.len { 18 | return ast.NodeError{ 19 | msg: 'expecting an alpha value after "/"' 20 | pos: node.pos 21 | } 22 | } else if i + 2 != func.children.len { 23 | return ast.NodeError{ 24 | msg: 'unexpected ident: expecting the alpha value to be the last argument to `rgb(a)`' 25 | pos: func.children[i + 2].pos() 26 | } 27 | } 28 | 29 | // check if next node is a percentage value 30 | alpha := func.children[i + 1] 31 | if alpha !is ast.Dimension || (alpha is ast.Dimension && alpha.unit != '%') { 32 | return ast.NodeError{ 33 | msg: 'expecting a percentage value' 34 | pos: alpha.pos() 35 | } 36 | } 37 | // convert 80% to 0-255 38 | rgbas << u8((alpha as ast.Dimension).value.f64() / 100 * 255) 39 | break 40 | } else if node.kind != .comma { 41 | return ast.NodeError{ 42 | msg: 'unexpected operator: expecting a number or alpha value' 43 | pos: node.pos 44 | } 45 | } 46 | } 47 | ast.Number { 48 | rgbas << node.value.u8() 49 | } 50 | // TODO: calc & css variables 51 | else { 52 | return ast.NodeError{ 53 | msg: 'unexpected ident' 54 | pos: node.pos() 55 | } 56 | } 57 | } 58 | } 59 | 60 | // set alpha to 100% when only given `rgb` 61 | if rgbas.len == 3 { 62 | rgbas << 255 63 | } else if rgbas.len != 4 { 64 | return ast.NodeError{ 65 | msg: 'expecting 3 or 4 arguments for function `rgb(a)` not "${rgbas.len}"' 66 | pos: func.pos 67 | } 68 | } 69 | 70 | return datatypes.Color{ 71 | r: rgbas[0] 72 | g: rgbas[1] 73 | b: rgbas[2] 74 | a: rgbas[3] 75 | } 76 | } 77 | 78 | pub fn (pv &PropertyValidator) validate_fn_url(fn_name string, raw_value ast.Function) !css.Url { 79 | if raw_value.children.len == 0 || raw_value.children.len > 2 { 80 | return ast.NodeError{ 81 | msg: 'the function "${fn_name}" expects 1 or 2 arguments' 82 | pos: raw_value.pos 83 | } 84 | } 85 | 86 | mut url := css.Url{} 87 | 88 | first_val := raw_value.children[0] 89 | if first_val is ast.String { 90 | url.value = first_val.value 91 | } else if first_val is ast.Hash { 92 | url.value = first_val.value 93 | url.kind = .element 94 | return url 95 | } else { 96 | return ast.NodeError{ 97 | msg: 'invalid value for function "${fn_name}"' 98 | pos: first_val.pos() 99 | } 100 | } 101 | 102 | if url.value[0] == `#` { 103 | url.kind = .element 104 | url.value = url.value[1..] 105 | } else if url.value.starts_with('data:') { 106 | url.kind = .data 107 | } else if url.value.starts_with('http') { 108 | url.kind = .link 109 | } else { 110 | url.kind = .file 111 | } 112 | 113 | return url 114 | } 115 | 116 | pub fn (pv &PropertyValidator) validate_fn_gradients(prop_name string, gradient_name string, raw_value ast.Function) !css.Gradient { 117 | if raw_value.children.len < 2 { 118 | return ast.NodeError{ 119 | msg: 'the function "${gradient_name}" expects at least 2 arguments' 120 | pos: raw_value.pos 121 | } 122 | } 123 | 124 | mut gradient := css.Gradient{} 125 | gradient.kind = match gradient_name { 126 | 'linear-gradient' { 127 | .linear 128 | } 129 | 'radial-gradient' { 130 | .radial 131 | } 132 | 'repeating-linear-gradient' { 133 | .repeating_linear 134 | } 135 | 'repeating-radial-gradient' { 136 | .repeating_radial 137 | } 138 | else { 139 | return ast.NodeError{ 140 | msg: 'expecting a gradient function not "${gradient_name}"\n${errors.did_you_mean(valid_gradient_functions)}' 141 | pos: raw_value.pos 142 | } 143 | } 144 | } 145 | 146 | mut color_count := 0 147 | for i := 0; i < raw_value.children.len; { 148 | mut errored := false 149 | mut is_direction := false 150 | 151 | mut gradient_color := ?css.ColorValue(none) 152 | mut gradient_size := ?css.DimensionValue(none) 153 | 154 | mut j := i 155 | // this inner loop processes children until a "," is reached 156 | for j < raw_value.children.len { 157 | child := raw_value.children[j] 158 | j++ 159 | 160 | if child is ast.Operator && child.kind == .comma { 161 | break 162 | } 163 | 164 | // skip until the next comma if an error has occured 165 | if errored { 166 | continue 167 | } 168 | 169 | match child { 170 | ast.Ident { 171 | if child.name == 'to' { 172 | if gradient.gradient_values.len > 0 { 173 | // gradient(red, to left, green) 174 | pv.error_with_pos('unexpected ident "to": only the first gradient value can be a direction', 175 | child.pos) 176 | errored = true 177 | continue 178 | } else if is_direction { 179 | // gradient(to to) 180 | pv.error_with_pos('expecting a direction after "to".\n${errors.did_you_mean(gradient_directions)}', 181 | child.pos) 182 | errored = true 183 | continue 184 | } else { 185 | is_direction = true 186 | } 187 | } else if child.name in gradient_directions { 188 | if gradient.gradient_values.len > 0 { 189 | // gradient(red, to left, green) 190 | pv.error_with_pos('unexpected ident "${child.name}": only the first gradient value can be a direction', 191 | child.pos) 192 | errored = true 193 | continue 194 | } else if !is_direction { 195 | // gradient(right, ) 196 | pv.error_with_pos('expecting the keyword "to" before a direction.', 197 | child.pos) 198 | errored = true 199 | } else { 200 | match child.name { 201 | 'top' { gradient.directions.set(.top) } 202 | 'left' { gradient.directions.set(.left) } 203 | 'right' { gradient.directions.set(.right) } 204 | 'bottom' { gradient.directions.set(.bottom) } 205 | else {} 206 | } 207 | } 208 | } else { 209 | // gradient(red green) 210 | if gradient_color != none { 211 | pv.error_with_pos('you can only have 1 color per gradient value', 212 | child.pos) 213 | errored = true 214 | continue 215 | } else { 216 | gradient_color = pv.validate_single_color_prop(gradient_name, 217 | ast.Value{ 218 | pos: child.pos 219 | children: [child] 220 | }) or { 221 | if err is ast.NodeError { 222 | pv.error_with_pos(err.msg(), err.pos) 223 | } 224 | errored = true 225 | continue 226 | } 227 | color_count++ 228 | } 229 | } 230 | } 231 | ast.Hash { 232 | // gradient(#aaa #bbb) 233 | if gradient_color != none { 234 | pv.error_with_pos('you can only have 1 color per gradient value', 235 | child.pos) 236 | errored = true 237 | continue 238 | } else { 239 | gradient_color = pv.validate_single_color_prop(gradient_name, 240 | ast.Value{ 241 | pos: child.pos 242 | children: [child] 243 | }) or { 244 | if err is ast.NodeError { 245 | pv.error_with_pos(err.msg(), err.pos) 246 | } 247 | errored = true 248 | continue 249 | } 250 | color_count++ 251 | } 252 | } 253 | ast.Dimension { 254 | // gradient(5px 4px) 255 | if gradient_size != none { 256 | pv.error_with_pos('you can only have 1 dimension per gradient value', 257 | child.pos) 258 | errored = true 259 | continue 260 | } else if gradient_color == none { 261 | // gradient(10px red) 262 | pv.error_with_pos('expecting a color before a dimension value', 263 | child.pos) 264 | errored = true 265 | continue 266 | } else { 267 | gradient_size = pv.validate_single_dimension_prop(gradient_name, 268 | ast.Value{ 269 | pos: child.pos 270 | children: [child] 271 | }) or { 272 | if err is ast.NodeError { 273 | pv.error_with_pos(err.msg(), err.pos) 274 | } 275 | errored = true 276 | continue 277 | } 278 | } 279 | } 280 | else {} 281 | } 282 | } 283 | i = j 284 | 285 | if !errored { 286 | // gradient(to, ) 287 | if is_direction && int(gradient.directions) == 0 { 288 | pv.error_with_pos('expecting a direction after "to".\n${errors.did_you_mean(gradient_directions)}', 289 | raw_value.children[i - 1].pos()) 290 | continue 291 | } 292 | 293 | if color := gradient_color { 294 | gradient.gradient_values << css.GradientValue{ 295 | color: color 296 | size: gradient_size 297 | } 298 | } 299 | } 300 | } 301 | 302 | if color_count < 2 { 303 | return ast.NodeError{ 304 | msg: 'expecting at least 2 color values in a gradient function' 305 | pos: raw_value.pos 306 | } 307 | } 308 | 309 | return gradient 310 | } 311 | -------------------------------------------------------------------------------- /css/checker/rule.v: -------------------------------------------------------------------------------- 1 | module checker 2 | 3 | import css 4 | import css.ast 5 | 6 | // pub fn (mut c Checker) validate_rule(rule ast.RuleNode) !css.Rule { 7 | // // TODO: make this faster?? 8 | // selectors := c.validate_selector_list(rule.selector.children)! 9 | 10 | // if c.tmp_declarations[rule.declaration_idx].len != 0 { 11 | // return css.Rule{ 12 | // specificity: css.Specificity.from_selectors(selectors) 13 | // selectors: selectors 14 | // declarations: c.tmp_declarations[rule.declaration_idx].clone() 15 | // } 16 | // } else { 17 | // decls := c.validate_declarations(c.table.raw_declarations[rule.declaration_idx]) 18 | // c.tmp_declarations[rule.declaration_idx] = decls.clone() 19 | // return css.Rule{ 20 | // specificity: css.Specificity.from_selectors(selectors) 21 | // selectors: selectors 22 | // declarations: decls 23 | // } 24 | // } 25 | // } 26 | 27 | pub fn (mut c Checker) validate_rule(rule ast.Rule) ! { 28 | if rule.prelude is ast.Raw { 29 | return ast.NodeError{ 30 | msg: 'invalid selector list' 31 | pos: rule.prelude.pos 32 | } 33 | } 34 | 35 | mut valid_selector_list := [][]css.Selector{} 36 | 37 | selector_list := rule.prelude as ast.SelectorList 38 | for selector in selector_list.children { 39 | if selector is ast.Selector { 40 | if list := c.validate_selector_list(selector.children) { 41 | valid_selector_list << [list] 42 | } else { 43 | if err is ast.NodeError { 44 | c.error_with_pos(err.msg(), err.pos) 45 | } else { 46 | c.error(err.msg()) 47 | } 48 | } 49 | } else { 50 | return ast.NodeError{ 51 | msg: 'invalid selectors' 52 | pos: selector.pos() 53 | } 54 | } 55 | } 56 | 57 | valid_declarations := c.validate_declarations(rule.block.declarations) 58 | 59 | for selectors in valid_selector_list { 60 | c.rules << css.Rule{ 61 | specificity: css.Specificity.from_selectors(selectors) 62 | selectors: selectors 63 | declarations: valid_declarations 64 | } 65 | } 66 | } 67 | 68 | pub fn (mut c Checker) validate_selector_list(selectors []ast.Node) ![]css.Selector { 69 | mut valid_selectors := []css.Selector{} 70 | 71 | for i, s in selectors { 72 | valid_selectors << match s { 73 | ast.ClassSelector { 74 | css.Selector(css.Class(s.name)) 75 | } 76 | ast.TypeSelector { 77 | css.Type(s.name) 78 | } 79 | ast.IdSelector { 80 | css.Id(s.name) 81 | } 82 | ast.AttributeSelector { 83 | mut attr := css.Attribute{ 84 | name: s.name.name 85 | matcher: s.matcher 86 | } 87 | if v := s.value { 88 | attr.value = v.value 89 | } 90 | 91 | attr 92 | } 93 | ast.PseudoSelector { 94 | match s { 95 | ast.PseudoClassSelector { 96 | if s.children.len > 0 && s.name !in pseudo_class_selectors_functions { 97 | return ast.NodeError{ 98 | msg: 'pseudo class selector "${s.name}" is not a function' 99 | pos: s.pos 100 | } 101 | } else if s.children.len == 0 && s.name in pseudo_class_selectors_functions { 102 | return ast.NodeError{ 103 | msg: 'expected pseudo class selector "${s.name}" to be a function' 104 | pos: s.pos 105 | } 106 | } 107 | // TODO: verify name + function arguments 108 | valid_list := c.validate_selector_list(s.children)! 109 | css.Selector(css.PseudoClass{ 110 | name: s.name 111 | children: valid_list 112 | }) 113 | } 114 | ast.PseudoElementSelector { 115 | if s.children.len > 0 && s.name !in pseudo_element_selectors_functions { 116 | return ast.NodeError{ 117 | msg: 'pseudo element selector "${s.name}" is not a function' 118 | pos: s.pos 119 | } 120 | } else if s.children.len == 0 121 | && s.name in pseudo_element_selectors_functions { 122 | return ast.NodeError{ 123 | msg: 'expected pseudo element selector "${s.name}" to be a function' 124 | pos: s.pos 125 | } 126 | } 127 | valid_list := c.validate_selector_list(s.children)! 128 | css.PseudoElement{ 129 | name: s.name 130 | children: valid_list 131 | } 132 | } 133 | else { 134 | return ast.NodeError{ 135 | msg: 'invalid or unsupported selector' 136 | pos: s.pos 137 | } 138 | } 139 | } 140 | } 141 | ast.Combinator { 142 | if i == selectors.len - 1 { 143 | return ast.NodeError{ 144 | msg: 'unexpected combinator: a combinator cannot be the last part of a CSS selector' 145 | pos: s.pos 146 | } 147 | } 148 | css.Combinator(s.kind) 149 | } 150 | else { 151 | return ast.NodeError{ 152 | msg: 'invalid or unsupported selector' 153 | pos: s.pos() 154 | } 155 | } 156 | } 157 | } 158 | 159 | return valid_selectors 160 | } 161 | -------------------------------------------------------------------------------- /css/checker/tests/fn_gradient.css: -------------------------------------------------------------------------------- 1 | 2 | .unkown_gradient { 3 | background-image: unkown-gradient(to right, red, green); 4 | } 5 | 6 | .direction_must_be_first_argument { 7 | background-image: linear-gradient(#ff0000, to left); 8 | background-image: linear-gradient(red, bottom); 9 | } 10 | 11 | .double_or_missing_values { 12 | background-image: linear-gradient(to, #aaa, #aaa); 13 | background-image: linear-gradient(to to right, #aaa, #aaa); 14 | background-image: linear-gradient(red green); 15 | background-image: linear-gradient(#aaa #bbb); 16 | background-image: linear-gradient(red 10px 20px, green); 17 | background-image: linear-gradient(10px, red, green); 18 | } 19 | 20 | .minimal_2_colors { 21 | background-image: linear-gradient(to right, green); 22 | } -------------------------------------------------------------------------------- /css/checker/tests/fn_gradient.out: -------------------------------------------------------------------------------- 1 | css/checker/tests/fn_gradient.css:3:23: error: expecting a gradient function not "unkown-gradient" 2 | Did you mean `linear-gradient`, `radial-gradient`, `repeating-linear-gradient`, `repeating-radial-gradient`? 3 | 1 | 4 | 2 | .unkown_gradient { 5 | 3 | background-image: unkown-gradient(to right, red, green); 6 | | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 7 | 4 | } 8 | 5 | 9 | css/checker/tests/fn_gradient.css:7:48: error: unexpected ident "to": only the first gradient value can be a direction 10 | 5 | 11 | 6 | .direction_must_be_first_argument { 12 | 7 | background-image: linear-gradient(#ff0000, to left); 13 | | ~~ 14 | 8 | background-image: linear-gradient(red, bottom); 15 | 9 | } 16 | css/checker/tests/fn_gradient.css:7:23: error: expecting at least 2 color values in a gradient function 17 | 5 | 18 | 6 | .direction_must_be_first_argument { 19 | 7 | background-image: linear-gradient(#ff0000, to left); 20 | | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 21 | 8 | background-image: linear-gradient(red, bottom); 22 | 9 | } 23 | css/checker/tests/fn_gradient.css:8:44: error: unexpected ident "bottom": only the first gradient value can be a direction 24 | 6 | .direction_must_be_first_argument { 25 | 7 | background-image: linear-gradient(#ff0000, to left); 26 | 8 | background-image: linear-gradient(red, bottom); 27 | | ~~~~~~ 28 | 9 | } 29 | 10 | 30 | css/checker/tests/fn_gradient.css:8:23: error: expecting at least 2 color values in a gradient function 31 | 6 | .direction_must_be_first_argument { 32 | 7 | background-image: linear-gradient(#ff0000, to left); 33 | 8 | background-image: linear-gradient(red, bottom); 34 | | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 35 | 9 | } 36 | 10 | 37 | css/checker/tests/fn_gradient.css:12:41: error: expecting a direction after "to". 38 | Did you mean `left`, `top`, `right`, `bottom`? 39 | 10 | 40 | 11 | .double_or_missing_values { 41 | 12 | background-image: linear-gradient(to, #aaa, #aaa); 42 | | ^ 43 | 13 | background-image: linear-gradient(to to right, #aaa, #aaa); 44 | 14 | background-image: linear-gradient(red green); 45 | css/checker/tests/fn_gradient.css:13:42: error: expecting a direction after "to". 46 | Did you mean `left`, `top`, `right`, `bottom`? 47 | 11 | .double_or_missing_values { 48 | 12 | background-image: linear-gradient(to, #aaa, #aaa); 49 | 13 | background-image: linear-gradient(to to right, #aaa, #aaa); 50 | | ~~ 51 | 14 | background-image: linear-gradient(red green); 52 | 15 | background-image: linear-gradient(#aaa #bbb); 53 | css/checker/tests/fn_gradient.css:14:43: error: you can only have 1 color per gradient value 54 | 12 | background-image: linear-gradient(to, #aaa, #aaa); 55 | 13 | background-image: linear-gradient(to to right, #aaa, #aaa); 56 | 14 | background-image: linear-gradient(red green); 57 | | ~~~~~ 58 | 15 | background-image: linear-gradient(#aaa #bbb); 59 | 16 | background-image: linear-gradient(red 10px 20px, green); 60 | css/checker/tests/fn_gradient.css:14:23: error: expecting at least 2 color values in a gradient function 61 | 12 | background-image: linear-gradient(to, #aaa, #aaa); 62 | 13 | background-image: linear-gradient(to to right, #aaa, #aaa); 63 | 14 | background-image: linear-gradient(red green); 64 | | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 65 | 15 | background-image: linear-gradient(#aaa #bbb); 66 | 16 | background-image: linear-gradient(red 10px 20px, green); 67 | css/checker/tests/fn_gradient.css:15:44: error: you can only have 1 color per gradient value 68 | 13 | background-image: linear-gradient(to to right, #aaa, #aaa); 69 | 14 | background-image: linear-gradient(red green); 70 | 15 | background-image: linear-gradient(#aaa #bbb); 71 | | ~~~~ 72 | 16 | background-image: linear-gradient(red 10px 20px, green); 73 | 17 | background-image: linear-gradient(10px, red, green); 74 | css/checker/tests/fn_gradient.css:15:23: error: expecting at least 2 color values in a gradient function 75 | 13 | background-image: linear-gradient(to to right, #aaa, #aaa); 76 | 14 | background-image: linear-gradient(red green); 77 | 15 | background-image: linear-gradient(#aaa #bbb); 78 | | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 79 | 16 | background-image: linear-gradient(red 10px 20px, green); 80 | 17 | background-image: linear-gradient(10px, red, green); 81 | css/checker/tests/fn_gradient.css:16:48: error: you can only have 1 dimension per gradient value 82 | 14 | background-image: linear-gradient(red green); 83 | 15 | background-image: linear-gradient(#aaa #bbb); 84 | 16 | background-image: linear-gradient(red 10px 20px, green); 85 | | ~~~~ 86 | 17 | background-image: linear-gradient(10px, red, green); 87 | 18 | } 88 | css/checker/tests/fn_gradient.css:17:39: error: expecting a color before a dimension value 89 | 15 | background-image: linear-gradient(#aaa #bbb); 90 | 16 | background-image: linear-gradient(red 10px 20px, green); 91 | 17 | background-image: linear-gradient(10px, red, green); 92 | | ~~~~ 93 | 18 | } 94 | 19 | 95 | css/checker/tests/fn_gradient.css:21:23: error: expecting at least 2 color values in a gradient function 96 | 19 | 97 | 20 | .minimal_2_colors { 98 | 21 | background-image: linear-gradient(to right, green); 99 | | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 100 | 22 | } -------------------------------------------------------------------------------- /css/checker/tests/fn_rgb.css: -------------------------------------------------------------------------------- 1 | 2 | .optional_comma { 3 | color: rgb(1 1 1); 4 | color: rgb(1, 1, 1); 5 | color: rgba(1 1 1 1); 6 | color: rgba(1, 1, 1, 1); 7 | } 8 | 9 | .unexpected_operator { 10 | color: rgb(1 + 2, 3, 4); 11 | } 12 | 13 | .alpha_test { 14 | color: rgb(0 0 0 / 80%); 15 | color: rgb(0 0 0 / ); 16 | color: rgb(0 0 0 / 80% 4); 17 | color: rgb(0 0 0 / 20px); 18 | } 19 | 20 | .wrong_nr_arguments{ 21 | color: rgb(0 0 0 0 0); 22 | color: rgb(0 ,0); 23 | } -------------------------------------------------------------------------------- /css/checker/tests/fn_rgb.out: -------------------------------------------------------------------------------- 1 | css/checker/tests/fn_rgb.css:10:18: error: unexpected operator: expecting a number or alpha value 2 | 8 | 3 | 9 | .unexpected_operator { 4 | 10 | color: rgb(1 + 2, 3, 4); 5 | | ^ 6 | 11 | } 7 | 12 | 8 | css/checker/tests/fn_rgb.css:15:22: error: expecting an alpha value after "/" 9 | 13 | .alpha_test { 10 | 14 | color: rgb(0 0 0 / 80%); 11 | 15 | color: rgb(0 0 0 / ); 12 | | ^ 13 | 16 | color: rgb(0 0 0 / 80% 4); 14 | 17 | color: rgb(0 0 0 / 20px); 15 | css/checker/tests/fn_rgb.css:16:28: error: unexpected ident: expecting the alpha value to be the last argument to `rgb(a)` 16 | 14 | color: rgb(0 0 0 / 80%); 17 | 15 | color: rgb(0 0 0 / ); 18 | 16 | color: rgb(0 0 0 / 80% 4); 19 | | ^ 20 | 17 | color: rgb(0 0 0 / 20px); 21 | 18 | } 22 | css/checker/tests/fn_rgb.css:17:24: error: expecting a percentage value 23 | 15 | color: rgb(0 0 0 / ); 24 | 16 | color: rgb(0 0 0 / 80% 4); 25 | 17 | color: rgb(0 0 0 / 20px); 26 | | ~~~~ 27 | 18 | } 28 | 19 | 29 | css/checker/tests/fn_rgb.css:21:12: error: expecting 3 or 4 arguments for function `rgb(a)` not "5" 30 | 19 | 31 | 20 | .wrong_nr_arguments{ 32 | 21 | color: rgb(0 0 0 0 0); 33 | | ~~~~~~~~~~~~~~ 34 | 22 | color: rgb(0 ,0); 35 | 23 | } 36 | css/checker/tests/fn_rgb.css:22:12: error: expecting 3 or 4 arguments for function `rgb(a)` not "2" 37 | 20 | .wrong_nr_arguments{ 38 | 21 | color: rgb(0 0 0 0 0); 39 | 22 | color: rgb(0 ,0); 40 | | ~~~~~~~~~ 41 | 23 | } -------------------------------------------------------------------------------- /css/checker/tests/fn_url.css: -------------------------------------------------------------------------------- 1 | 2 | .no_string { 3 | background-image: url(https://example.com/images/myImg.jpg); 4 | background-image: url(data:image/png;base64,iRxVBo); 5 | background-image: url(myFont.woff); 6 | background-image: url(#IDofSVGpath); 7 | } 8 | 9 | .invalid_child { 10 | background-image: url(10px); 11 | } -------------------------------------------------------------------------------- /css/checker/tests/fn_url.out: -------------------------------------------------------------------------------- 1 | css/checker/tests/fn_url.css:10:27: error: invalid value for function "url" 2 | 8 | 3 | 9 | .invalid_child { 4 | 10 | background-image: url(10px); 5 | | ~~~~ 6 | 11 | } -------------------------------------------------------------------------------- /css/checker/tests/prop_alpha_value.css: -------------------------------------------------------------------------------- 1 | 2 | .alpha { 3 | opacity: 0.5; 4 | opacity: 50%; 5 | } 6 | 7 | .invalid_alpha { 8 | opacity: -2; 9 | opacity: 2; 10 | opacity: 110%; 11 | opacity: -20%; 12 | } -------------------------------------------------------------------------------- /css/checker/tests/prop_alpha_value.out: -------------------------------------------------------------------------------- 1 | css/checker/tests/prop_alpha_value.css:8:14: error: Alpha value must be a percentage between 0-100% or between 0 and 1 2 | 6 | 3 | 7 | .invalid_alpha { 4 | 8 | opacity: -2; 5 | | ~~~ 6 | 9 | opacity: 2; 7 | 10 | opacity: 110%; 8 | css/checker/tests/prop_alpha_value.css:9:14: error: Alpha value must be a percentage between 0-100% or between 0 and 1 9 | 7 | .invalid_alpha { 10 | 8 | opacity: -2; 11 | 9 | opacity: 2; 12 | | ~~ 13 | 10 | opacity: 110%; 14 | 11 | opacity: -20%; 15 | css/checker/tests/prop_alpha_value.css:10:14: error: Alpha value must be a percentage between 0-100% or between 0 and 1 16 | 8 | opacity: -2; 17 | 9 | opacity: 2; 18 | 10 | opacity: 110%; 19 | | ~~~~~ 20 | 11 | opacity: -20%; 21 | 12 | } 22 | css/checker/tests/prop_alpha_value.css:11:14: error: Alpha value must be a percentage between 0-100% or between 0 and 1 23 | 9 | opacity: 2; 24 | 10 | opacity: 110%; 25 | 11 | opacity: -20%; 26 | | ~~~~~ 27 | 12 | } -------------------------------------------------------------------------------- /css/checker/tests/prop_border.css: -------------------------------------------------------------------------------- 1 | 2 | .nr_values { 3 | border-color: red green blue black orange; 4 | border-top-color: green red; 5 | 6 | border-style: dotted dashed double groove inset; 7 | border-top-style: dotted dashed; 8 | } 9 | 10 | .invalid_values { 11 | border-color: 10px; 12 | border-style: "wjat!!"; 13 | } 14 | 15 | .border { 16 | border: solid solid; 17 | border: 2px 2px; 18 | border: red red; 19 | border: solid red 2px green; 20 | border: inherit; 21 | } 22 | 23 | .radius { 24 | border-radius: 10px / ; 25 | border-radius: 1px 2px 3px 4px 5px; 26 | border-top-left-radius: 20px / ; 27 | border-top-left-radius: 1px 2px 3px 4px; 28 | border-top-left-radius: 1px 2px 3px; 29 | } -------------------------------------------------------------------------------- /css/checker/tests/prop_border.out: -------------------------------------------------------------------------------- 1 | css/checker/tests/prop_border.css:3:19: error: property "border-color" can have a maximum of 4 values! 2 | 1 | 3 | 2 | .nr_values { 4 | 3 | border-color: red green blue black orange; 5 | | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 6 | 4 | border-top-color: green red; 7 | 5 | 8 | css/checker/tests/prop_border.css:4:23: error: property "border-top-color" only expects 1 value! 9 | 2 | .nr_values { 10 | 3 | border-color: red green blue black orange; 11 | 4 | border-top-color: green red; 12 | | ~~~~~~~~~~ 13 | 5 | 14 | 6 | border-style: dotted dashed double groove inset; 15 | css/checker/tests/prop_border.css:6:19: error: property "border-style" can have a maximum of 4 values! 16 | 4 | border-top-color: green red; 17 | 5 | 18 | 6 | border-style: dotted dashed double groove inset; 19 | | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 20 | 7 | border-top-style: dotted dashed; 21 | 8 | } 22 | css/checker/tests/prop_border.css:7:23: error: property "border-top-style" only expects 1 value! 23 | 5 | 24 | 6 | border-style: dotted dashed double groove inset; 25 | 7 | border-top-style: dotted dashed; 26 | | ~~~~~~~~~~~~~~ 27 | 8 | } 28 | 9 | 29 | css/checker/tests/prop_border.css:11:19: error: invalid value for property "border-color"! 30 | 9 | 31 | 10 | .invalid_values { 32 | 11 | border-color: 10px; 33 | | ~~~~ 34 | 12 | border-style: "wjat!!"; 35 | 13 | } 36 | css/checker/tests/prop_border.css:12:19: error: invalid value for property "border-style"! 37 | 10 | .invalid_values { 38 | 11 | border-color: 10px; 39 | 12 | border-style: "wjat!!"; 40 | | ~~~~~~~~ 41 | 13 | } 42 | 14 | 43 | css/checker/tests/prop_border.css:16:19: error: only 1 border-style value is allowed for property "border" 44 | 14 | 45 | 15 | .border { 46 | 16 | border: solid solid; 47 | | ~~~~~ 48 | 17 | border: 2px 2px; 49 | 18 | border: red red; 50 | css/checker/tests/prop_border.css:17:17: error: only 1 dimension value is allowed for property "border" 51 | 15 | .border { 52 | 16 | border: solid solid; 53 | 17 | border: 2px 2px; 54 | | ~~~ 55 | 18 | border: red red; 56 | 19 | border: solid red 2px green; 57 | css/checker/tests/prop_border.css:18:17: error: only 1 color value is allowed for property "border" 58 | 16 | border: solid solid; 59 | 17 | border: 2px 2px; 60 | 18 | border: red red; 61 | | ~~~ 62 | 19 | border: solid red 2px green; 63 | 20 | border: inherit; 64 | css/checker/tests/prop_border.css:19:13: error: property "border" can have a maximum of 3 values! 65 | 17 | border: 2px 2px; 66 | 18 | border: red red; 67 | 19 | border: solid red 2px green; 68 | | ~~~~~~~~~~~~~~~~~~~~ 69 | 20 | border: inherit; 70 | 21 | } 71 | css/checker/tests/prop_border.css:24:25: error: expecting a length or percentage after "/" 72 | 22 | 73 | 23 | .radius { 74 | 24 | border-radius: 10px / ; 75 | | ^ 76 | 25 | border-radius: 1px 2px 3px 4px 5px; 77 | 26 | border-top-left-radius: 20px / ; 78 | css/checker/tests/prop_border.css:25:36: error: property "border-radius" can have a maximum of 4 values 79 | 23 | .radius { 80 | 24 | border-radius: 10px / ; 81 | 25 | border-radius: 1px 2px 3px 4px 5px; 82 | | ~~~ 83 | 26 | border-top-left-radius: 20px / ; 84 | 27 | border-top-left-radius: 1px 2px 3px 4px; 85 | css/checker/tests/prop_border.css:26:29: error: invalid value for property "border-top-left-radius" 86 | 24 | border-radius: 10px / ; 87 | 25 | border-radius: 1px 2px 3px 4px 5px; 88 | 26 | border-top-left-radius: 20px / ; 89 | | ~~~~~~~~ 90 | 27 | border-top-left-radius: 1px 2px 3px 4px; 91 | 28 | border-top-left-radius: 1px 2px 3px; 92 | css/checker/tests/prop_border.css:27:29: error: invalid value for property "border-top-left-radius" 93 | 25 | border-radius: 1px 2px 3px 4px 5px; 94 | 26 | border-top-left-radius: 20px / ; 95 | 27 | border-top-left-radius: 1px 2px 3px 4px; 96 | | ~~~~~~~~~~~~~~~~ 97 | 28 | border-top-left-radius: 1px 2px 3px; 98 | 29 | } 99 | css/checker/tests/prop_border.css:28:33: error: expecting a "/" 100 | 26 | border-top-left-radius: 20px / ; 101 | 27 | border-top-left-radius: 1px 2px 3px 4px; 102 | 28 | border-top-left-radius: 1px 2px 3px; 103 | | ~~~ 104 | 29 | } -------------------------------------------------------------------------------- /css/checker/tests/prop_color.css: -------------------------------------------------------------------------------- 1 | .multiple_values { 2 | color: green, red; 3 | } 4 | 5 | .unsupported_function { 6 | color: bla(255, 255, 255); 7 | } -------------------------------------------------------------------------------- /css/checker/tests/prop_color.out: -------------------------------------------------------------------------------- 1 | css/checker/tests/prop_color.css:2:12: error: property "color" only expects 1 value! 2 | 1 | .multiple_values { 3 | 2 | color: green, red; 4 | | ~~~~~~~~~~~ 5 | 3 | } 6 | 4 | 7 | css/checker/tests/prop_color.css:6:12: error: unsupported function "bla" for property "color". 8 | Did you mean `rgb`, `rgba`, `hsl`? 9 | 4 | 10 | 5 | .unsupported_function { 11 | 6 | color: bla(255, 255, 255); 12 | | ~~~~~~~~~~~~~~~~~~ 13 | 7 | } -------------------------------------------------------------------------------- /css/checker/tests/prop_endings_color.css: -------------------------------------------------------------------------------- 1 | 2 | .border { 3 | border-color: red; 4 | border-top-color: green; 5 | outline-color: yellow; 6 | text-decoration-color: blue; 7 | } -------------------------------------------------------------------------------- /css/checker/tests/prop_endings_color.out: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Casper64/css/404d52181f09ebf0a172479d47508f599ce58115/css/checker/tests/prop_endings_color.out -------------------------------------------------------------------------------- /css/checker/tests/prop_flex.css: -------------------------------------------------------------------------------- 1 | 2 | .flex-direction { 3 | flex-direction: column row; 4 | flex-direction: 'column'; 5 | } 6 | 7 | .flex-size { 8 | flex-grow: 1.0; 9 | flex-grow: auto 2.0; 10 | flex-shrink: -1; 11 | flex-grow: 'blabla'; 12 | } 13 | 14 | .flex-wrap { 15 | flex-wrap: nowrap other; 16 | flex-wrap: 2; 17 | } 18 | 19 | .flex-flow { 20 | flex-flow: column row-reverse wrap; 21 | flex-flow: column other; 22 | flex-flow: 'what'; 23 | } 24 | 25 | .flex { 26 | flex: 0 0 0 0; 27 | } 28 | 29 | .flex-valid { 30 | flex: 0 0 0; 31 | flex: 1 1 20%; 32 | flex: auto; 33 | } -------------------------------------------------------------------------------- /css/checker/tests/prop_flex.out: -------------------------------------------------------------------------------- 1 | css/checker/tests/prop_flex.css:3:21: error: property "flex-direction" can only have 1 value! 2 | 1 | 3 | 2 | .flex-direction { 4 | 3 | flex-direction: column row; 5 | | ~~~~~~~~~~~ 6 | 4 | flex-direction: 'column'; 7 | 5 | } 8 | css/checker/tests/prop_flex.css:4:21: error: invalid value for property "flex-direction" 9 | 2 | .flex-direction { 10 | 3 | flex-direction: column row; 11 | 4 | flex-direction: 'column'; 12 | | ~~~~~~~~ 13 | 5 | } 14 | 6 | 15 | css/checker/tests/prop_flex.css:9:16: error: property "flex-grow" can only have 1 value! 16 | 7 | .flex-size { 17 | 8 | flex-grow: 1.0; 18 | 9 | flex-grow: auto 2.0; 19 | | ~~~~~~~~~ 20 | 10 | flex-shrink: -1; 21 | 11 | flex-grow: 'blabla'; 22 | css/checker/tests/prop_flex.css:10:18: error: value for property "flex-shrink" cannot be negative! 23 | 8 | flex-grow: 1.0; 24 | 9 | flex-grow: auto 2.0; 25 | 10 | flex-shrink: -1; 26 | | ~~ 27 | 11 | flex-grow: 'blabla'; 28 | 12 | } 29 | css/checker/tests/prop_flex.css:11:16: error: invalid value for property "flex-grow" 30 | 9 | flex-grow: auto 2.0; 31 | 10 | flex-shrink: -1; 32 | 11 | flex-grow: 'blabla'; 33 | | ~~~~~~~~ 34 | 12 | } 35 | 13 | 36 | css/checker/tests/prop_flex.css:15:16: error: property "flex-wrap" can only have 1 value! 37 | 13 | 38 | 14 | .flex-wrap { 39 | 15 | flex-wrap: nowrap other; 40 | | ~~~~~~~~~~~~~ 41 | 16 | flex-wrap: 2; 42 | 17 | } 43 | css/checker/tests/prop_flex.css:16:16: error: invalid value for property "flex-wrap" 44 | 14 | .flex-wrap { 45 | 15 | flex-wrap: nowrap other; 46 | 16 | flex-wrap: 2; 47 | | ^ 48 | 17 | } 49 | 18 | 50 | css/checker/tests/prop_flex.css:20:16: error: invalid value for property "flex-flow" 51 | 18 | 52 | 19 | .flex-flow { 53 | 20 | flex-flow: column row-reverse wrap; 54 | | ~~~~~~~~~~~~~~~~~~~~~~~~ 55 | 21 | flex-flow: column other; 56 | 22 | flex-flow: 'what'; 57 | css/checker/tests/prop_flex.css:21:23: error: invalid value for property "flex-flow" 58 | 19 | .flex-flow { 59 | 20 | flex-flow: column row-reverse wrap; 60 | 21 | flex-flow: column other; 61 | | ~~~~~ 62 | 22 | flex-flow: 'what'; 63 | 23 | } 64 | css/checker/tests/prop_flex.css:22:16: error: invalid value for property "flex-flow" 65 | 20 | flex-flow: column row-reverse wrap; 66 | 21 | flex-flow: column other; 67 | 22 | flex-flow: 'what'; 68 | | ~~~~~~~ 69 | 23 | } 70 | 24 | 71 | css/checker/tests/prop_flex.css:26:11: error: invalid value for property "flex" 72 | 24 | 73 | 25 | .flex { 74 | 26 | flex: 0 0 0 0; 75 | | ~~~~~~~~ 76 | 27 | } 77 | 28 | -------------------------------------------------------------------------------- /css/checker/tests/prop_font.css: -------------------------------------------------------------------------------- 1 | 2 | .font-family { 3 | font-family: 'Courier New', Courier, monospace; 4 | font-family: 10px; 5 | font-family: 'Whaaat' + COurier; 6 | } 7 | 8 | .font-stretch { 9 | font-stretch: 20px; 10 | font-stretch: normal; 11 | font-stretch: other double; 12 | font-stretch: 'test'; 13 | } 14 | 15 | .font-weight { 16 | font-weight: -1; 17 | font-weight: 1001; 18 | font-weight: 500; 19 | font-weight: 'omg'; 20 | } -------------------------------------------------------------------------------- /css/checker/tests/prop_font.out: -------------------------------------------------------------------------------- 1 | css/checker/tests/prop_font.css:4:18: error: invalid value for property "font-family" 2 | 2 | .font-family { 3 | 3 | font-family: 'Courier New', Courier, monospace; 4 | 4 | font-family: 10px; 5 | | ~~~~ 6 | 5 | font-family: 'Whaaat' + COurier; 7 | 6 | } 8 | css/checker/tests/prop_font.css:5:27: error: unexpected operator 9 | 3 | font-family: 'Courier New', Courier, monospace; 10 | 4 | font-family: 10px; 11 | 5 | font-family: 'Whaaat' + COurier; 12 | | ^ 13 | 6 | } 14 | 7 | 15 | css/checker/tests/prop_font.css:9:19: error: invalid value for property "font-stretch" 16 | 7 | 17 | 8 | .font-stretch { 18 | 9 | font-stretch: 20px; 19 | | ~~~~ 20 | 10 | font-stretch: normal; 21 | 11 | font-stretch: other double; 22 | css/checker/tests/prop_font.css:11:19: error: property "font-stretch" only expects 1 value! 23 | 9 | font-stretch: 20px; 24 | 10 | font-stretch: normal; 25 | 11 | font-stretch: other double; 26 | | ~~~~~~~~~~~~~ 27 | 12 | font-stretch: 'test'; 28 | 13 | } 29 | css/checker/tests/prop_font.css:12:19: error: invalid value for property "font-stretch" 30 | 10 | font-stretch: normal; 31 | 11 | font-stretch: other double; 32 | 12 | font-stretch: 'test'; 33 | | ~~~~~~ 34 | 13 | } 35 | 14 | 36 | css/checker/tests/prop_font.css:16:18: error: Font weight must be between [1, 1000] 37 | 14 | 38 | 15 | .font-weight { 39 | 16 | font-weight: -1; 40 | | ~~ 41 | 17 | font-weight: 1001; 42 | 18 | font-weight: 500; 43 | css/checker/tests/prop_font.css:17:18: error: Font weight must be between [1, 1000] 44 | 15 | .font-weight { 45 | 16 | font-weight: -1; 46 | 17 | font-weight: 1001; 47 | | ~~~~ 48 | 18 | font-weight: 500; 49 | 19 | font-weight: 'omg'; 50 | css/checker/tests/prop_font.css:19:18: error: invalid value for property "font-weight" 51 | 17 | font-weight: 1001; 52 | 18 | font-weight: 500; 53 | 19 | font-weight: 'omg'; 54 | | ~~~~~ 55 | 20 | } -------------------------------------------------------------------------------- /css/checker/tests/prop_gap.css: -------------------------------------------------------------------------------- 1 | 2 | .gap { 3 | gap: 10px 20px 30px; 4 | gap: what; 5 | gap: 20vw; 6 | } -------------------------------------------------------------------------------- /css/checker/tests/prop_gap.out: -------------------------------------------------------------------------------- 1 | css/checker/tests/prop_gap.css:3:10: error: invalid value for property "gap" 2 | 1 | 3 | 2 | .gap { 4 | 3 | gap: 10px 20px 30px; 5 | | ~~~~~~~~~~~~~~~ 6 | 4 | gap: what; 7 | 5 | gap: 20vw; -------------------------------------------------------------------------------- /css/checker/tests/prop_margin_padding.css: -------------------------------------------------------------------------------- 1 | 2 | .margin { 3 | margin: 10px 20px 30px 40px; 4 | margin: 10px 20px; 5 | margin: 100px; 6 | margin: 10px 20px 30px; 7 | } 8 | 9 | .padding { 10 | padding: 10px 20px 30px 40px; 11 | padding: 10px 20px; 12 | padding: 100px; 13 | padding: 10px 20px 30px; 14 | } 15 | 16 | .unsupported { 17 | padding-inline-start: 10px; 18 | margin-block-end: 10%; 19 | } 20 | 21 | .too_many { 22 | padding: 10px 20px 30px 40px 50px; 23 | margin: 10px 20px 30px 40px 50px; 24 | } -------------------------------------------------------------------------------- /css/checker/tests/prop_margin_padding.out: -------------------------------------------------------------------------------- 1 | css/checker/tests/prop_margin_padding.css:17:5: error: unsupported property "padding-inline-start"! Check the "CAN_I_USE.md" for a list of supported properties 2 | 15 | 3 | 16 | .unsupported { 4 | 17 | padding-inline-start: 10px; 5 | | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 6 | 18 | margin-block-end: 10%; 7 | 19 | } 8 | css/checker/tests/prop_margin_padding.css:18:5: error: unsupported property "margin-block-end"! Check the "CAN_I_USE.md" for a list of supported properties 9 | 16 | .unsupported { 10 | 17 | padding-inline-start: 10px; 11 | 18 | margin-block-end: 10%; 12 | | ~~~~~~~~~~~~~~~~~~~~~~ 13 | 19 | } 14 | 20 | 15 | css/checker/tests/prop_margin_padding.css:22:34: error: property "padding" can have a maximum of 4 values 16 | 20 | 17 | 21 | .too_many { 18 | 22 | padding: 10px 20px 30px 40px 50px; 19 | | ~~~~ 20 | 23 | margin: 10px 20px 30px 40px 50px; 21 | 24 | } 22 | css/checker/tests/prop_margin_padding.css:23:33: error: property "margin" can have a maximum of 4 values 23 | 21 | .too_many { 24 | 22 | padding: 10px 20px 30px 40px 50px; 25 | 23 | margin: 10px 20px 30px 40px 50px; 26 | | ~~~~ 27 | 24 | } -------------------------------------------------------------------------------- /css/checker/tests/prop_overflow.css: -------------------------------------------------------------------------------- 1 | 2 | .invalid_property { 3 | overflow-z: hidden; 4 | } 5 | 6 | .invalid_keywords_len { 7 | overflow: a b c; 8 | overflow-x: a b; 9 | overflow-y: a b; 10 | } 11 | 12 | .non-keyword { 13 | overflow: 10px; 14 | } -------------------------------------------------------------------------------- /css/checker/tests/prop_overflow.out: -------------------------------------------------------------------------------- 1 | css/checker/tests/prop_overflow.css:3:5: error: unsupported property "overflow-z"! Check the "CAN_I_USE.md" for a list of supported properties 2 | 1 | 3 | 2 | .invalid_property { 4 | 3 | overflow-z: hidden; 5 | | ~~~~~~~~~~~~~~~~~~~ 6 | 4 | } 7 | 5 | 8 | css/checker/tests/prop_overflow.css:7:15: error: expecting 1 or 2 values for property "overflow" not 3 9 | 5 | 10 | 6 | .invalid_keywords_len { 11 | 7 | overflow: a b c; 12 | | ~~~~~~ 13 | 8 | overflow-x: a b; 14 | 9 | overflow-y: a b; 15 | css/checker/tests/prop_overflow.css:8:17: error: property "overflow-x" only expects 1 value! 16 | 6 | .invalid_keywords_len { 17 | 7 | overflow: a b c; 18 | 8 | overflow-x: a b; 19 | | ~~~~ 20 | 9 | overflow-y: a b; 21 | 10 | } 22 | css/checker/tests/prop_overflow.css:9:17: error: property "overflow-y" only expects 1 value! 23 | 7 | overflow: a b c; 24 | 8 | overflow-x: a b; 25 | 9 | overflow-y: a b; 26 | | ~~~~ 27 | 10 | } 28 | 11 | 29 | css/checker/tests/prop_overflow.css:13:15: error: invalid value for property "overflow" 30 | 11 | 31 | 12 | .non-keyword { 32 | 13 | overflow: 10px; 33 | | ~~~~ 34 | 14 | } -------------------------------------------------------------------------------- /css/checker/tests/prop_shadow.css: -------------------------------------------------------------------------------- 1 | 2 | .invalid_length_values { 3 | box-shadow: 10px red; 4 | box-shadow: 1px 2px 3px 4px 5px red; 5 | } 6 | 7 | .no-color { 8 | box-shadow: 10px 20px; 9 | } 10 | 11 | .multiple-keywords { 12 | box-shadow: inset inset 10px 20px red; 13 | box-shadow: inset red 10px 20px; 14 | box-shadow: inset 10px 20px red; 15 | } 16 | 17 | .color-functions { 18 | box-shadow: inset 2px 4px 4px 8px rgba(0 0 0 / 20%); 19 | } -------------------------------------------------------------------------------- /css/checker/tests/prop_shadow.out: -------------------------------------------------------------------------------- 1 | css/checker/tests/prop_shadow.css:3:17: error: expecting 0, 2, 3 or 4 length values for property "box-shadow" not 1 2 | 1 | 3 | 2 | .invalid_length_values { 4 | 3 | box-shadow: 10px red; 5 | | ~~~~~~~~~ 6 | 4 | box-shadow: 1px 2px 3px 4px 5px red; 7 | 5 | } 8 | css/checker/tests/prop_shadow.css:4:17: error: expecting 0, 2, 3 or 4 length values for property "box-shadow" not 5 9 | 2 | .invalid_length_values { 10 | 3 | box-shadow: 10px red; 11 | 4 | box-shadow: 1px 2px 3px 4px 5px red; 12 | | ~~~~~~~~~~~~~~~~~~~~~~~~ 13 | 5 | } 14 | 6 | 15 | css/checker/tests/prop_shadow.css:8:17: error: a shadow property must specify a color! 16 | 6 | 17 | 7 | .no-color { 18 | 8 | box-shadow: 10px 20px; 19 | | ~~~~~~~~~~ 20 | 9 | } 21 | 10 | 22 | css/checker/tests/prop_shadow.css:12:23: error: keyword "inset" is already used 23 | 10 | 24 | 11 | .multiple-keywords { 25 | 12 | box-shadow: inset inset 10px 20px red; 26 | | ~~~~~ 27 | 13 | box-shadow: inset red 10px 20px; 28 | 14 | box-shadow: inset 10px 20px red; -------------------------------------------------------------------------------- /css/checker/tests/prop_single_dim.css: -------------------------------------------------------------------------------- 1 | 2 | .units { 3 | width: 100px; 4 | width: 100vw; 5 | width: 20vh; 6 | width: 10em; 7 | width: 20rem; 8 | width: 110%; 9 | } 10 | 11 | .unsupported_unit { 12 | width: 100in; 13 | } 14 | 15 | .multiple_vals { 16 | width: 20px, 40px; 17 | width: 100vw 20vh; 18 | } 19 | 20 | .all_props { 21 | width: 10px; 22 | height: 10px; 23 | top: 10px; 24 | left: 10px; 25 | bottom: 10px; 26 | right: 10px; 27 | } 28 | -------------------------------------------------------------------------------- /css/checker/tests/prop_single_dim.out: -------------------------------------------------------------------------------- 1 | css/checker/tests/prop_single_dim.css:12:12: error: unsupported unit "in". 2 | Did you mean `em`, `rem`, `px`, `vw`, `vh`? 3 | 10 | 4 | 11 | .unsupported_unit { 5 | 12 | width: 100in; 6 | | ~~~~~ 7 | 13 | } 8 | 14 | 9 | css/checker/tests/prop_single_dim.css:16:12: error: property "width" only expects 1 value! 10 | 14 | 11 | 15 | .multiple_vals { 12 | 16 | width: 20px, 40px; 13 | | ~~~~~~~~~~~ 14 | 17 | width: 100vw 20vh; 15 | 18 | } 16 | css/checker/tests/prop_single_dim.css:17:12: error: property "width" only expects 1 value! 17 | 15 | .multiple_vals { 18 | 16 | width: 20px, 40px; 19 | 17 | width: 100vw 20vh; 20 | | ~~~~~~~~~~~ 21 | 18 | } 22 | 19 | -------------------------------------------------------------------------------- /css/checker/tests/prop_starts_single_dim.css: -------------------------------------------------------------------------------- 1 | 2 | .margin { 3 | margin-left: 10px; 4 | margin-right: 20px; 5 | margin-top: 30px; 6 | margin-bottom: 40px; 7 | } 8 | 9 | .padding { 10 | padding-left: 10px; 11 | padding-right: 20px; 12 | padding-top: 30px; 13 | padding-bottom: 40px; 14 | } -------------------------------------------------------------------------------- /css/checker/tests/prop_starts_single_dim.out: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Casper64/css/404d52181f09ebf0a172479d47508f599ce58115/css/checker/tests/prop_starts_single_dim.out -------------------------------------------------------------------------------- /css/checker/tests/prop_text.css: -------------------------------------------------------------------------------- 1 | 2 | .text-overflow { 3 | text-overflow: a b c; 4 | text-overflow: other "[..]"; 5 | text-overflow: ellipsis "[..]"; 6 | text-overflow: 10px; 7 | } 8 | 9 | .text-combine-upright { 10 | text-combine-upright: a b c; 11 | text-combine-upright: all 5; 12 | text-combine-upright: digits 4; 13 | text-combine-upright: "say what"; 14 | } -------------------------------------------------------------------------------- /css/checker/tests/prop_text.out: -------------------------------------------------------------------------------- 1 | css/checker/tests/prop_text.css:3:20: error: property "text-overflow" can have 1 or 2 values 2 | 1 | 3 | 2 | .text-overflow { 4 | 3 | text-overflow: a b c; 5 | | ~~~~~~ 6 | 4 | text-overflow: other "[..]"; 7 | 5 | text-overflow: ellipsis "[..]"; 8 | css/checker/tests/prop_text.css:4:20: error: only the keyword "ellipsis" can have a second value for property "text-overflow" 9 | 2 | .text-overflow { 10 | 3 | text-overflow: a b c; 11 | 4 | text-overflow: other "[..]"; 12 | | ~~~~~~~~~~~~~ 13 | 5 | text-overflow: ellipsis "[..]"; 14 | 6 | text-overflow: 10px; 15 | css/checker/tests/prop_text.css:6:20: error: expecting a keyword 16 | 4 | text-overflow: other "[..]"; 17 | 5 | text-overflow: ellipsis "[..]"; 18 | 6 | text-overflow: 10px; 19 | | ~~~~ 20 | 7 | } 21 | 8 | 22 | css/checker/tests/prop_text.css:10:27: error: property "text-combine-upright" can have 1 or 2 values 23 | 8 | 24 | 9 | .text-combine-upright { 25 | 10 | text-combine-upright: a b c; 26 | | ~~~~~~ 27 | 11 | text-combine-upright: all 5; 28 | 12 | text-combine-upright: digits 4; 29 | css/checker/tests/prop_text.css:11:27: error: only the keyword "digits" can have a second value for property "text-combine-upright" 30 | 9 | .text-combine-upright { 31 | 10 | text-combine-upright: a b c; 32 | 11 | text-combine-upright: all 5; 33 | | ~~~~~~ 34 | 12 | text-combine-upright: digits 4; 35 | 13 | text-combine-upright: "say what"; 36 | css/checker/tests/prop_text.css:13:27: error: expecting a keyword 37 | 11 | text-combine-upright: all 5; 38 | 12 | text-combine-upright: digits 4; 39 | 13 | text-combine-upright: "say what"; 40 | | ~~~~~~~~~~ 41 | 14 | } -------------------------------------------------------------------------------- /css/checker/tests/test_program.v: -------------------------------------------------------------------------------- 1 | import css.ast 2 | import css.checker 3 | import css.parser 4 | import css.pref 5 | import os 6 | 7 | fn main() { 8 | if os.args.len < 2 { 9 | panic('usage: v run main.v CSS_FILE [--strict]') 10 | } 11 | 12 | is_strict := '--strict' in os.args 13 | mut prefs := pref.Preferences{ 14 | is_strict: is_strict 15 | } 16 | 17 | mut table := &ast.Table{} 18 | mut p := parser.Parser.new(prefs) 19 | p.table = table 20 | tree := p.parse_file(os.args[1]) 21 | 22 | checker.validate(tree, mut table, prefs) or {} 23 | } 24 | -------------------------------------------------------------------------------- /css/checker/tests/unsupported_property.css: -------------------------------------------------------------------------------- 1 | .test { 2 | invalid: green; 3 | } -------------------------------------------------------------------------------- /css/checker/tests/unsupported_property.out: -------------------------------------------------------------------------------- 1 | css/checker/tests/unsupported_property.css:2:5: error: unsupported property "invalid"! Check the "CAN_I_USE.md" for a list of supported properties 2 | 1 | .test { 3 | 2 | invalid: green; 4 | | ~~~~~~~~~~~~~~~ 5 | 3 | } -------------------------------------------------------------------------------- /css/datatypes/README.md: -------------------------------------------------------------------------------- 1 | # CSS.datatypes 2 | 3 | This module contains structs and types to represent the types of CSS values and functions. 4 | The purpose of this submodule is to provide a constant base of types and structures from 5 | which CSS propreties can be represented. 6 | 7 | Reference: [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS) subsection `Reference -> types`. -------------------------------------------------------------------------------- /css/datatypes/datatypes.v: -------------------------------------------------------------------------------- 1 | module datatypes 2 | 3 | import strconv 4 | import strings 5 | 6 | // See https://developer.mozilla.org/en-US/docs/Web/CSS/length#absolute_length_units 7 | pub enum Unit { 8 | em 9 | rem 10 | vh 11 | vw 12 | px 13 | } 14 | 15 | pub struct Length { 16 | pub mut: 17 | amount f64 18 | unit Unit 19 | } 20 | 21 | // Percentage indicates a value between 0 and 100%, so 0.0 - 1.0 22 | pub type Percentage = f64 23 | 24 | // compatible with gx.Color 25 | pub struct Color { 26 | pub mut: 27 | r u8 28 | g u8 29 | b u8 30 | a u8 = 255 31 | } 32 | 33 | pub fn Color.from_hex(_hex string) Color { 34 | hex := _hex.to_lower() 35 | if hex.len == 3 { 36 | return Color{ 37 | r: u8(strconv.parse_int(strings.repeat(hex[0], 2), 16, 0) or { 0 }) 38 | g: u8(strconv.parse_int(strings.repeat(hex[1], 2), 16, 0) or { 0 }) 39 | b: u8(strconv.parse_int(strings.repeat(hex[2], 2), 16, 0) or { 0 }) 40 | a: 255 41 | } 42 | } else if hex.len == 4 { 43 | return Color{ 44 | r: u8(strconv.parse_int(strings.repeat(hex[0], 2), 16, 0) or { 0 }) 45 | g: u8(strconv.parse_int(strings.repeat(hex[1], 2), 16, 0) or { 0 }) 46 | b: u8(strconv.parse_int(strings.repeat(hex[2], 2), 16, 0) or { 0 }) 47 | a: u8(strconv.parse_int(strings.repeat(hex[3], 2), 16, 0) or { 0 }) 48 | } 49 | } else if hex.len == 6 { 50 | return Color{ 51 | r: u8(strconv.parse_int(hex[0..2], 16, 0) or { 0 }) 52 | g: u8(strconv.parse_int(hex[2..4], 16, 0) or { 0 }) 53 | b: u8(strconv.parse_int(hex[4..6], 16, 0) or { 0 }) 54 | a: 255 55 | } 56 | } else if hex.len == 8 { 57 | return Color{ 58 | r: u8(strconv.parse_int(hex[0..2], 16, 0) or { 0 }) 59 | g: u8(strconv.parse_int(hex[2..4], 16, 0) or { 0 }) 60 | b: u8(strconv.parse_int(hex[4..6], 16, 0) or { 0 }) 61 | a: u8(strconv.parse_int(hex[6..8], 16, 0) or { 0 }) 62 | } 63 | } else { 64 | // invalid hex color length is handled in the parser/checker 65 | // so no need to do that here 66 | return Color{} 67 | } 68 | } 69 | 70 | pub fn (c Color) int() int { 71 | mut v := int(c.r) 72 | v <<= 8 73 | v += c.g 74 | v <<= 8 75 | v += c.b 76 | v <<= 8 77 | v += c.a 78 | return v 79 | } 80 | 81 | pub enum LineStyle { 82 | @none 83 | hidden 84 | dotted 85 | dashed 86 | solid 87 | double 88 | groove 89 | ridge 90 | inset 91 | outset 92 | } 93 | 94 | pub enum GradientKind { 95 | linear 96 | radial 97 | repeating_linear 98 | repeating_radial 99 | } 100 | 101 | @[flag] 102 | pub enum GradientDirection { 103 | top 104 | right 105 | left 106 | bottom 107 | } 108 | 109 | pub enum FlexDirectionKind { 110 | row 111 | row_reverse 112 | column 113 | column_reverse 114 | } 115 | 116 | pub enum FlexWrapKind { 117 | nowrap 118 | wrap 119 | wrap_reverse 120 | } 121 | 122 | pub enum FontStretchKind { 123 | normal 124 | ultra_condensed 125 | extra_condensed 126 | semi_condensed 127 | expanded 128 | extra_expanded 129 | ultra_expanded 130 | semi_expanded 131 | } 132 | -------------------------------------------------------------------------------- /css/datatypes/functions.v: -------------------------------------------------------------------------------- 1 | module datatypes 2 | 3 | pub struct CalcSum { 4 | } 5 | 6 | // evaluate returns evaluates the sum and returns the value in pixels 7 | pub fn (sum CalcSum) evaluate(em f64, rem f64, vh f64, vw f64) f64 { 8 | return 0.0 9 | } 10 | -------------------------------------------------------------------------------- /css/datatypes/tests/color_test.v: -------------------------------------------------------------------------------- 1 | import css.datatypes 2 | import gx 3 | 4 | fn test_color() { 5 | mut c := datatypes.Color.from_hex('fff') 6 | assert c == datatypes.Color{255, 255, 255, 255} 7 | 8 | c = datatypes.Color.from_hex('fffa') 9 | assert c == datatypes.Color{255, 255, 255, 170} 10 | 11 | c = datatypes.Color.from_hex('abcdef') 12 | assert c == datatypes.Color{171, 205, 239, 255} 13 | 14 | c = datatypes.Color.from_hex('abcdef05') 15 | assert c == datatypes.Color{171, 205, 239, 5} 16 | } 17 | 18 | fn test_color_to_int() { 19 | assert datatypes.Color{255, 10, 32, 11}.int() == 0xff0a200b 20 | } 21 | 22 | fn test_compatible_with_gx() { 23 | c := datatypes.Color{10, 20, 30, 40} 24 | 25 | assert gx.Color{ 26 | ...c 27 | } == gx.Color{10, 20, 30, 40} 28 | } 29 | -------------------------------------------------------------------------------- /css/errors/errors.v: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2023 Alexander Medvednikov. All rights reserved. 2 | // Use of this source code is governed by an MIT license 3 | // that can be found in the LICENSE file. 4 | module errors 5 | 6 | import css.token 7 | import os 8 | import strings 9 | import term 10 | import v.mathutil as mu 11 | 12 | // error_context_before - how many lines of source context to print before the pointer line 13 | // error_context_after - ^^^ same, but after 14 | const error_context_before = 2 15 | const error_context_after = 2 16 | 17 | @[params] 18 | pub struct CompilerMessage { 19 | msg string 20 | details string 21 | file_path string 22 | pos token.Pos 23 | } 24 | 25 | const normalised_workdir = os.wd_at_startup.replace('\\', '/') + '/' 26 | 27 | // NOTE: path_styled_for_error_messages will *always* use `/` in the error paths, no matter the OS, 28 | // to ensure stable compiler error output in the tests. 29 | pub fn path_styled_for_error_messages(path string) string { 30 | mut rpath := os.real_path(path) 31 | rpath = rpath.replace('\\', '/') 32 | 33 | if rpath.starts_with(errors.normalised_workdir) { 34 | rpath = rpath.replace_once(errors.normalised_workdir, '') 35 | } 36 | return rpath 37 | } 38 | 39 | pub fn formatted_error(kind string, omsg string, filepath string, pos token.Pos) string { 40 | emsg := omsg.replace('main.', '') 41 | path := path_styled_for_error_messages(filepath) 42 | position := if filepath.len > 0 { 43 | '${path}:${pos.line_nr + 1}:${mu.max(1, pos.col + 1)}:' 44 | } else { 45 | '' 46 | } 47 | 48 | final_position := bold(position) 49 | final_kind := bold(color(kind, kind)) 50 | final_msg := emsg 51 | 52 | // skip context for .min.css files 53 | if filepath.ends_with('.min.css') { 54 | return '${final_position} ${final_kind} ${final_msg}'.trim_space() 55 | } 56 | 57 | scontext := source_file_context(kind, filepath, pos).join('\n') 58 | final_context := if scontext.len > 0 { '\n${scontext}' } else { '' } 59 | 60 | return '${final_position} ${final_kind} ${final_msg}${final_context}'.trim_space() 61 | } 62 | 63 | pub fn show_compiler_message(kind string, err CompilerMessage) { 64 | eprintln(formatted_error(kind, err.msg, err.file_path, err.pos)) 65 | if err.details.len != 0 { 66 | eprintln(err.details) 67 | } 68 | } 69 | 70 | pub fn bold(msg string) string { 71 | // if !errors.emanager.support_color { 72 | // return msg 73 | // } 74 | return term.bold(msg) 75 | } 76 | 77 | pub fn color(kind string, msg string) string { 78 | // if !errors.emanager.support_color { 79 | // return msg 80 | // } 81 | if kind.contains('error') { 82 | return term.red(msg) 83 | } 84 | if kind.contains('notice') { 85 | return term.yellow(msg) 86 | } 87 | if kind.contains('details') { 88 | return term.bright_blue(msg) 89 | } 90 | return term.magenta(msg) 91 | } 92 | 93 | // set_source_for_path should be called for every file, over which you want to use errors.formatted_error 94 | pub fn set_source_for_path(path string, source string) []string { 95 | lines := source.split_into_lines() 96 | return lines 97 | } 98 | 99 | pub fn file2sourcelines(path string) []string { 100 | source := os.read_file(path) or { '' } 101 | res := set_source_for_path(path, source) 102 | return res 103 | } 104 | 105 | pub fn source_file_context(kind string, filepath string, pos token.Pos) []string { 106 | mut clines := []string{} 107 | source_lines := unsafe { file2sourcelines(filepath) } 108 | if source_lines.len == 0 { 109 | return clines 110 | } 111 | bline := mu.max(0, pos.line_nr - errors.error_context_before) 112 | aline := mu.max(0, mu.min(source_lines.len - 1, pos.line_nr + errors.error_context_after)) 113 | tab_spaces := ' ' 114 | for iline := bline; iline <= aline; iline++ { 115 | sline := source_lines[iline] 116 | start_column := mu.max(0, mu.min(pos.col, sline.len)) 117 | end_column := mu.max(0, mu.min(pos.col + mu.max(0, pos.len), sline.len)) 118 | cline := if iline == pos.line_nr { 119 | sline[..start_column] + color(kind, sline[start_column..end_column]) + 120 | sline[end_column..] 121 | } else { 122 | sline 123 | } 124 | clines << '${iline + 1:5d} | ' + cline.replace('\t', tab_spaces) 125 | // 126 | if iline == pos.line_nr { 127 | // The pointerline should have the same spaces/tabs as the offending 128 | // line, so that it prints the ^ character exactly on the *same spot* 129 | // where it is needed. That is the reason we can not just 130 | // use strings.repeat(` `, col) to form it. 131 | mut pointerline_builder := strings.new_builder(sline.len) 132 | for i := 0; i < start_column; { 133 | if sline[i].is_space() { 134 | pointerline_builder.write_u8(sline[i]) 135 | i++ 136 | } else { 137 | char_len := utf8_char_len(sline[i]) 138 | spaces := ' '.repeat(utf8_str_visible_length(sline[i..i + char_len])) 139 | pointerline_builder.write_string(spaces) 140 | i += char_len 141 | } 142 | } 143 | underline_len := utf8_str_visible_length(sline[start_column..end_column]) 144 | underline := if underline_len > 1 { '~'.repeat(underline_len) } else { '^' } 145 | pointerline_builder.write_string(bold(color(kind, underline))) 146 | clines << ' | ' + pointerline_builder.str().replace('\t', tab_spaces) 147 | } 148 | } 149 | return clines 150 | } 151 | 152 | pub fn did_you_mean(values []string) string { 153 | options := values.map(|v| term.bright_blue('`${v}`')).join(', ') 154 | return 'Did you mean ${options}?' 155 | } 156 | -------------------------------------------------------------------------------- /css/functions.v: -------------------------------------------------------------------------------- 1 | module css 2 | 3 | import css.datatypes 4 | 5 | pub enum UrlKind { 6 | link // http:// 7 | data // data:image/png;base64, 8 | file // myFont.woff 9 | element // #IDofSVGpath 10 | } 11 | 12 | pub struct Url { 13 | pub mut: 14 | kind UrlKind 15 | value string 16 | } 17 | 18 | pub fn (u1 Url) == (u2 Url) bool { 19 | if u1.kind != u2.kind { 20 | return false 21 | } 22 | 23 | return u1.value == u2.value 24 | } 25 | 26 | pub struct Gradient { 27 | pub mut: 28 | kind datatypes.GradientKind 29 | directions datatypes.GradientDirection 30 | gradient_values []GradientValue 31 | } 32 | 33 | pub struct GradientValue { 34 | color ColorValue 35 | size ?DimensionValue 36 | } 37 | -------------------------------------------------------------------------------- /css/gen/README.md: -------------------------------------------------------------------------------- 1 | # Gen 2 | 3 | This module provides a very minimal css minifier. It still needs a lot of work done. 4 | -------------------------------------------------------------------------------- /css/gen/block.v: -------------------------------------------------------------------------------- 1 | module gen 2 | 3 | import css.ast 4 | 5 | fn (mut g Gen) gen_block(block ast.Block) { 6 | g.indent_level++ 7 | defer { 8 | g.indent_level-- 9 | } 10 | 11 | g.out.write_string('${g.space_minify()}{') 12 | 13 | for i, decl in block.declarations { 14 | match decl { 15 | ast.Raw {} 16 | ast.Declaration { 17 | g.gen_declaration(decl, i == block.declarations.len - 1) 18 | } 19 | else {} 20 | } 21 | } 22 | 23 | if g.minify { 24 | g.out.write_string('}') 25 | } else { 26 | g.out.writeln('\n}\n') 27 | } 28 | } 29 | 30 | fn (mut g Gen) gen_declaration(decl ast.Declaration, is_last bool) { 31 | if !g.minify { 32 | g.write_indent() 33 | } 34 | g.out.write_string('${decl.property}:${g.space_minify()}') 35 | g.gen_decl_value(decl.value) 36 | 37 | if decl.important { 38 | g.out.write_string('${g.space_minify()}!important') 39 | } 40 | 41 | if (g.minify && !is_last) || !g.minify { 42 | g.out.write_string(';') 43 | } 44 | } 45 | 46 | fn (mut g Gen) gen_decl_value(value ast.Value) { 47 | for i, v in value.children { 48 | mut must_have_seperator := false 49 | match v { 50 | ast.Raw {} 51 | ast.Number { 52 | g.out.write_string('${v.value}') 53 | must_have_seperator = true 54 | } 55 | ast.Ident { 56 | g.out.write_string('${v.name}') 57 | must_have_seperator = true 58 | } 59 | ast.String { 60 | g.out.write_string('"${v.value}"') 61 | } 62 | ast.Hash { 63 | g.out.write_string('#${v.value}') 64 | must_have_seperator = true 65 | } 66 | ast.Operator { 67 | operator_str := match v.kind { 68 | .plus { 69 | must_have_seperator = true 70 | '+' 71 | } 72 | .min { 73 | must_have_seperator = true 74 | '-' 75 | } 76 | .mul { 77 | '*' 78 | } 79 | .div { 80 | '/' 81 | } 82 | .comma { 83 | ',' 84 | } 85 | } 86 | g.out.write_string('${operator_str}') 87 | if !must_have_seperator { 88 | g.out.write_string('${g.space_minify()}') 89 | } 90 | } 91 | ast.Parentheses { 92 | g.out.write_string('(') 93 | 94 | g.parentheses_depth++ 95 | g.gen_decl_value(ast.Value{ 96 | pos: v.pos 97 | children: v.children 98 | }) 99 | g.parentheses_depth-- 100 | 101 | g.out.write_string(')') 102 | } 103 | ast.Dimension { 104 | g.out.write_string('${v.value}${v.unit}') 105 | // TODO: check next child 106 | must_have_seperator = true 107 | } 108 | ast.Function { 109 | g.out.write_string('${v.name}(') 110 | 111 | g.parentheses_depth++ 112 | g.gen_decl_value(ast.Value{ 113 | pos: v.pos 114 | children: v.children 115 | }) 116 | g.parentheses_depth-- 117 | 118 | g.out.write_string(')') 119 | } 120 | else {} 121 | } 122 | // don't add a space before ";" 123 | if must_have_seperator && i != value.children.len - 1 { 124 | g.out.write_string(' ') 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /css/gen/gen.v: -------------------------------------------------------------------------------- 1 | module gen 2 | 3 | import css.ast 4 | import css.pref 5 | import strings 6 | 7 | @[params] 8 | pub struct GenOptions { 9 | minify bool 10 | } 11 | 12 | pub struct Gen { 13 | prefs pref.Preferences 14 | minify bool 15 | stylesheet &ast.StyleSheet 16 | mut: 17 | out strings.Builder 18 | indent_level int 19 | parentheses_depth int 20 | } 21 | 22 | pub fn generate(stylesheet &ast.StyleSheet, opts GenOptions, prefs pref.Preferences) string { 23 | mut global_g := &Gen{ 24 | prefs: prefs 25 | minify: opts.minify 26 | stylesheet: stylesheet 27 | out: strings.new_builder(640000) 28 | } 29 | 30 | return global_g.gen_file() 31 | } 32 | 33 | pub fn (mut g Gen) gen_file() string { 34 | for rule in g.stylesheet.rules { 35 | match rule { 36 | ast.AtRule { 37 | g.gen_at_rule(rule) 38 | } 39 | ast.Rule { 40 | g.gen_normal_rule(rule) 41 | } 42 | else {} 43 | } 44 | } 45 | 46 | out_str := g.out.str() 47 | unsafe { 48 | g.out.free() 49 | } 50 | return out_str 51 | } 52 | 53 | fn (mut g Gen) write_indent() { 54 | g.out.write_string('\n') 55 | for _ in 0 .. g.indent_level { 56 | g.out.write_string(' ') 57 | } 58 | } 59 | 60 | fn (mut g Gen) space_minify() string { 61 | if !g.minify { 62 | return ' ' 63 | } 64 | return '' 65 | } 66 | -------------------------------------------------------------------------------- /css/gen/rules.v: -------------------------------------------------------------------------------- 1 | module gen 2 | 3 | import css.ast 4 | 5 | pub fn (mut g Gen) gen_at_rule(rule ast.AtRule) { 6 | match rule.typ { 7 | .unkown {} 8 | .charset {} 9 | else {} 10 | } 11 | } 12 | 13 | fn (mut g Gen) gen_normal_rule(rule ast.Rule) { 14 | // generate selectors 15 | match rule.prelude { 16 | ast.Raw { 17 | g.out.writeln('/* prelude: ast.Raw */') 18 | g.out.writeln('${rule.prelude.value}${g.space_minify()}{') 19 | } 20 | ast.SelectorList { 21 | selector_list := rule.prelude as ast.SelectorList 22 | if rule.block.declarations.len == 0 { 23 | // skip css rule with no declarations 24 | return 25 | } 26 | 27 | for i, selector in selector_list.children { 28 | if selector is ast.Selector { 29 | g.gen_selector(selector) 30 | } 31 | if i != selector_list.children.len - 1 { 32 | if g.minify { 33 | g.out.write_string(',') 34 | } else { 35 | g.out.writeln(',') 36 | } 37 | } 38 | } 39 | } 40 | } 41 | g.gen_block(rule.block) 42 | } 43 | -------------------------------------------------------------------------------- /css/gen/selector.v: -------------------------------------------------------------------------------- 1 | module gen 2 | 3 | import css.ast 4 | 5 | fn (mut g Gen) gen_selector(selector ast.Selector) { 6 | for s in selector.children { 7 | match s { 8 | ast.ClassSelector { 9 | g.out.write_string('.${s.name}') 10 | } 11 | ast.TypeSelector { 12 | g.out.write_string(s.name) 13 | } 14 | ast.IdSelector { 15 | g.out.write_string('#${s.name}') 16 | } 17 | ast.Combinator { 18 | if s.kind != ' ' { 19 | g.out.write_string('${g.space_minify()}${s.kind}${g.space_minify()}') 20 | } else { 21 | g.out.write_string(' ') 22 | } 23 | } 24 | ast.PseudoSelector { 25 | g.gen_pseudo_selector(s) 26 | } 27 | ast.AttributeSelector { 28 | g.gen_attr_selector(s) 29 | } 30 | else {} 31 | } 32 | } 33 | } 34 | 35 | fn (mut g Gen) gen_pseudo_selector(s ast.PseudoSelector) { 36 | match s { 37 | ast.Raw {} 38 | ast.PseudoClassSelector, ast.PseudoElementSelector { 39 | g.out.write_string(':${s.name}') 40 | if s is ast.PseudoElementSelector { 41 | g.out.write_string(':${s.name}') 42 | } 43 | 44 | if s.children.len > 0 { 45 | g.out.write_string('(') 46 | g.gen_selector(ast.Selector{ 47 | pos: s.pos 48 | children: s.children 49 | }) 50 | g.out.write_string(')') 51 | } 52 | } 53 | } 54 | } 55 | 56 | fn (mut g Gen) gen_attr_selector(attr ast.AttributeSelector) { 57 | g.out.write_string('[${attr.name.name}') 58 | 59 | match attr.matcher { 60 | .exact { 61 | g.out.write_string('=') 62 | } 63 | .contains { 64 | g.out.write_string('*=') 65 | } 66 | .starts_with { 67 | g.out.write_string('^=') 68 | } 69 | .ends_with { 70 | g.out.write_string('$=') 71 | } 72 | .@none {} 73 | } 74 | 75 | if v := attr.value { 76 | g.out.write_string('"${v.value}"') 77 | } 78 | g.out.write_string(']') 79 | } 80 | -------------------------------------------------------------------------------- /css/gen/tests/gen_test.v: -------------------------------------------------------------------------------- 1 | import css.util as css_util 2 | import os 3 | 4 | const in_file = 'css/gen/tests/testdata/pretty.css' 5 | const expected_pretty_file = 'css/gen/tests/testdata/pretty_out.css' 6 | const expected_min_file = 'css/gen/tests/testdata/pretty_out.min.css' 7 | 8 | fn test_gen_pretty() { 9 | out_file := '${os.temp_dir()}/gen_pretty_out.css' 10 | println(out_file) 11 | css_util.prettify_file(in_file, out_file)! 12 | 13 | expected := os.read_file(expected_pretty_file)! 14 | found := os.read_file(out_file)! 15 | 16 | assert clean_line_endings(expected) == clean_line_endings(found) 17 | } 18 | 19 | fn test_gen_minify() { 20 | out_file := '${os.temp_dir()}/gen_minify_out.min.css' 21 | css_util.minify_file(in_file, out_file)! 22 | 23 | expected := os.read_file(expected_min_file)! 24 | found := os.read_file(out_file)! 25 | 26 | assert clean_line_endings(expected) == clean_line_endings(found) 27 | } 28 | 29 | fn clean_line_endings(s string) string { 30 | mut res := s.trim_space() 31 | res = res.replace(' \n', '\n') 32 | res = res.replace(' \r\n', '\n') 33 | res = res.replace('\r\n', '\n') 34 | res = res.trim('\n') 35 | return res 36 | } 37 | -------------------------------------------------------------------------------- /css/gen/tests/testdata/pretty.css: -------------------------------------------------------------------------------- 1 | 2 | .test, .other #id 3 | { 4 | color : green ; 5 | width: calc(100px*5+(20vw - 30vh)); 6 | } 7 | 8 | -------------------------------------------------------------------------------- /css/gen/tests/testdata/pretty_out.css: -------------------------------------------------------------------------------- 1 | .test, 2 | .other #id { 3 | color: green; 4 | width: calc(100px * 5 + (20vw - 30vh)); 5 | } -------------------------------------------------------------------------------- /css/gen/tests/testdata/pretty_out.min.css: -------------------------------------------------------------------------------- 1 | .test,.other #id{color:green;width:calc(100px*5 + (20vw - 30vh))} -------------------------------------------------------------------------------- /css/lexer/util.v: -------------------------------------------------------------------------------- 1 | module lexer 2 | 3 | pub fn is_name_char(c u8) bool { 4 | return (c >= `a` && c <= `z`) || (c >= `A` && c <= `Z`) || c == `_` || c == `-` 5 | } 6 | -------------------------------------------------------------------------------- /css/parser/at_rules.v: -------------------------------------------------------------------------------- 1 | module parser 2 | 3 | import css.ast 4 | 5 | pub fn (mut p Parser) at_decl() ast.AtRule { 6 | if p.tok.lit.len == 0 { 7 | p.error_with_pos('expecting a name after "@"', p.tok.pos()) 8 | p.next() 9 | return ast.AtRule{} 10 | } 11 | 12 | mut rule := match p.tok.lit { 13 | 'import' { 14 | p.error('@import rules can only occur at the top of the file!') 15 | ast.AtRule{} 16 | } 17 | 'charset' { 18 | p.parse_at_charset() or { 19 | p.recover_until(.semicolon) 20 | // ??? 21 | // skip ";" 22 | // ast.Raw{ 23 | // value: p.lexer.get_lit(err.pos.pos, p.tok.pos().pos) 24 | // pos: err.pos.extend(p.tok.pos()) 25 | // } 26 | ast.AtRule{} 27 | } 28 | } 29 | 'layer' { 30 | p.parse_at_layer() 31 | } 32 | 'media' { 33 | p.parse_at_media() 34 | } 35 | 'keyframes', '-webkit-keyframes' { 36 | p.parse_at_keyframes() or { 37 | p.skip_block() 38 | return ast.AtRule{} 39 | } 40 | } 41 | else { 42 | p.warn('unkown at-rule "${p.tok.lit}"') 43 | p.skip_block() 44 | return ast.AtRule{} 45 | } 46 | } 47 | p.next() 48 | return rule 49 | } 50 | 51 | pub fn (mut p Parser) parse_at_charset() !ast.AtRule { 52 | charset_start := p.tok.pos() 53 | // skip @charset 54 | p.next() 55 | 56 | mut charset := '' 57 | if p.tok.kind == .string && p.tok.meta == double_quote { 58 | charset = p.tok.lit 59 | } else { 60 | // @charset 'test'; 61 | return p.error('invalid charset rule: expecting a double quoted string after "@charset"') 62 | } 63 | 64 | // todo extrapolate in function? 65 | p.next() 66 | if p.tok.kind != .semicolon { 67 | return p.error('expecting a semicolon to close the at-rule') 68 | } 69 | if p.tok.pos != p.prev_tok.pos + p.prev_tok.len { 70 | return p.error('the ";" has to be the next character after "${charset}"') 71 | } 72 | 73 | return ast.AtRule{ 74 | pos: charset_start.extend(p.tok.pos()) 75 | typ: .charset 76 | prelude: [ast.String{ 77 | pos: p.tok.pos() 78 | value: charset 79 | }] 80 | } 81 | } 82 | 83 | pub fn (mut p Parser) parse_at_layer() ast.AtRule { 84 | layer_start := p.tok.pos() 85 | // skip @layer 86 | p.next() 87 | 88 | mut layer_names := []ast.Node{} 89 | mut comma_count := 0 90 | mut children := []ast.Node{} 91 | 92 | for { 93 | match p.tok.kind { 94 | .name { 95 | if comma_count != layer_names.len { 96 | p.warn('expecting a comma separated list. Missing "," before "${p.tok.lit}"') 97 | } 98 | layer_names << ast.Ident{ 99 | pos: p.tok.pos() 100 | name: p.tok.lit 101 | } 102 | } 103 | .comma { 104 | comma_count++ 105 | if comma_count != layer_names.len { 106 | p.warn('expecting a name, ";" or "{" not ","') 107 | comma_count-- 108 | } 109 | } 110 | .lcbr { 111 | p.next() 112 | for { 113 | if p.tok.kind == .eof || p.tok.kind == .rcbr { 114 | break 115 | } 116 | rule := p.top_rule() 117 | children << rule 118 | } 119 | if p.tok.kind != .rcbr { 120 | p.error('expecting "}". Did you forget to close the previous block?') 121 | } 122 | break 123 | } 124 | .semicolon { 125 | break 126 | } 127 | else { 128 | p.unexpected(got: ' token ${p.tok.kind}') 129 | return ast.AtRule{} 130 | } 131 | } 132 | p.next() 133 | } 134 | 135 | return ast.AtRule{ 136 | pos: layer_start.extend(p.tok.pos()) 137 | typ: .layer 138 | prelude: layer_names 139 | children: children 140 | } 141 | } 142 | 143 | pub fn (mut p Parser) parse_at_media() ast.AtRule { 144 | media_start := p.tok.pos() 145 | // skip @media 146 | p.next() 147 | 148 | mut media_list_pos := p.tok.pos() 149 | mut query_start := p.tok.pos() 150 | mut queries := []ast.MediaQuery{} 151 | mut current_children := []ast.Node{} 152 | 153 | mut media_rules := []ast.Node{} 154 | 155 | for { 156 | match p.tok.kind { 157 | .name { 158 | current_children << ast.Ident{ 159 | pos: p.tok.pos() 160 | name: p.tok.lit 161 | } 162 | } 163 | .lpar { 164 | if c := p.parse_at_media_feature() { 165 | current_children << c 166 | } 167 | } 168 | .lcbr { 169 | queries << ast.MediaQuery{ 170 | pos: query_start.extend(p.tok.pos()) 171 | children: current_children 172 | } 173 | media_list_pos = media_list_pos.extend(p.tok.pos()) 174 | p.next() 175 | for { 176 | if p.tok.kind == .eof || p.tok.kind == .rcbr { 177 | break 178 | } 179 | rule := p.top_rule() 180 | media_rules << rule 181 | } 182 | if p.tok.kind != .rcbr { 183 | p.error('expecting "}". Did you forget to close the previous block?') 184 | } 185 | break 186 | } 187 | .comma { 188 | if current_children.len == 0 || p.peek_tok.kind == .lcbr { 189 | p.warn('expecting a media query or "{"') 190 | } else { 191 | queries << ast.MediaQuery{ 192 | pos: query_start.extend(p.tok.pos()) 193 | children: current_children 194 | } 195 | current_children.clear() 196 | query_start = p.peek_tok.pos() 197 | } 198 | } 199 | else { 200 | p.unexpected(got: 'token ${p.tok.kind}') 201 | } 202 | } 203 | p.next() 204 | } 205 | 206 | return ast.AtRule{ 207 | pos: media_start.extend(p.tok.pos()) 208 | typ: .media 209 | prelude: [ast.MediaQueryList{ 210 | pos: media_list_pos 211 | children: queries 212 | }] 213 | children: media_rules 214 | } 215 | } 216 | 217 | pub fn (mut p Parser) parse_at_media_feature() !ast.MediaFeature { 218 | feature_start := p.tok.pos() 219 | // skip "(" 220 | p.next() 221 | 222 | mut media_name := '' 223 | mut media_value := ?ast.Node(none) 224 | mut has_value := false 225 | 226 | for { 227 | match p.tok.kind { 228 | .name { 229 | if !has_value { 230 | media_name = p.tok.lit 231 | } else if media_value != none { 232 | // @media (hover: hover hover) 233 | return p.error('unexpected token: expecting ")"') 234 | } else { 235 | media_value = ast.Ident{ 236 | pos: p.tok.pos() 237 | name: p.tok.lit 238 | } 239 | } 240 | } 241 | .number { 242 | if media_value != none { 243 | // @media (hover : 600px 600px) 244 | return p.error('unexpected token: expecting ")"') 245 | } 246 | media_value = p.parse_dimension() 247 | } 248 | .rpar { 249 | if has_value && media_value == none { 250 | // @media (min-width:) 251 | return p.error('unexpected token: expecting a term') 252 | } 253 | break 254 | } 255 | .colon { 256 | if has_value { 257 | // @media (min-width: test :) 258 | return p.error('unexpected token: expecting ")"') 259 | } 260 | has_value = true 261 | } 262 | else { 263 | return p.unexpected() 264 | } 265 | } 266 | p.next() 267 | } 268 | 269 | return ast.MediaFeature{ 270 | pos: feature_start.extend(p.tok.pos()) 271 | name: media_name 272 | value: media_value 273 | } 274 | } 275 | 276 | pub fn (mut p Parser) parse_at_keyframes() !ast.AtRule { 277 | keyframes_start := p.tok.pos() 278 | // skip @keyframes 279 | p.next() 280 | 281 | if p.tok.kind != .name { 282 | // @keyframes {} 283 | return p.error('expecting the name of the keyframes rule') 284 | } 285 | keyframe_name := ast.Ident{ 286 | pos: p.tok.pos() 287 | name: p.tok.lit 288 | } 289 | 290 | p.next() 291 | if p.tok.kind != .lcbr { 292 | // @keyframes name ... } 293 | return p.error('expecting "{"') 294 | } 295 | 296 | // skip "{" 297 | p.next() 298 | 299 | mut keyframe_rules := []ast.Node{} 300 | mut current_percentage := ?string(none) 301 | 302 | for { 303 | match p.tok.kind { 304 | .name { 305 | if current_percentage != none { 306 | p.error('expecting "{"') 307 | p.recover_until(.rcbr) 308 | current_percentage = none 309 | } else if p.tok.lit != 'from' && p.tok.lit != 'to' { 310 | // .test {} 311 | p.error('expecting a percentage or "from" or "to"') 312 | p.skip_block() 313 | continue 314 | } else { 315 | current_percentage = if p.tok.lit == 'from' { 316 | '0' 317 | } else { 318 | '100' 319 | } 320 | } 321 | } 322 | .number { 323 | if current_percentage != none { 324 | p.error('expecting "{"') 325 | p.recover_until(.rcbr) 326 | current_percentage = none 327 | } else if p.peek_tok.kind != .percentage { 328 | // 50 {} 329 | p.error('expecting a percentage or "from" or "to"') 330 | p.skip_block() 331 | continue 332 | } else { 333 | current_percentage = p.tok.lit 334 | // skip "50%" 335 | p.next() 336 | } 337 | } 338 | .lcbr { 339 | if percentage := current_percentage { 340 | rule_start := p.tok.pos() 341 | // skip "{" 342 | p.next() 343 | declarations := p.parse_declaration_block() 344 | if p.tok.kind != .rcbr { 345 | p.error('expecting "}". Did you forget to close the previous block?') 346 | break 347 | } else { 348 | keyframe_rules << ast.KeyframesRule{ 349 | pos: rule_start.extend(p.tok.pos()) 350 | percentage: percentage 351 | declarations: declarations 352 | } 353 | 354 | current_percentage = none 355 | } 356 | } else { 357 | p.error('expecting a percentage or "from" or "to" before a keyframes rule') 358 | p.skip_block() 359 | continue 360 | } 361 | } 362 | .rcbr { 363 | break 364 | } 365 | else { 366 | return p.error('expecting a keyframes rule') 367 | } 368 | } 369 | p.next() 370 | } 371 | 372 | return ast.AtRule{ 373 | pos: keyframes_start.extend(p.tok.pos()) 374 | typ: .keyframes 375 | prelude: [keyframe_name] 376 | children: keyframe_rules 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /css/parser/declaration.v: -------------------------------------------------------------------------------- 1 | module parser 2 | 3 | import css.ast 4 | 5 | pub fn (mut p Parser) parse_declaration_block() []ast.Node { 6 | mut declarations := []ast.Node{} 7 | 8 | for { 9 | if p.tok.kind == .rcbr || p.tok.kind == .eof { 10 | break 11 | } 12 | 13 | if decl := p.parse_declaration() { 14 | declarations << decl 15 | } else { 16 | // continue to next line 17 | for p.tok.kind !in [.semicolon, .rcbr, .eof] { 18 | p.next() 19 | } 20 | // prevent parser from getting stuck e.g. "color: green;;" 21 | if p.tok.kind == .semicolon { 22 | p.next() 23 | } 24 | 25 | if err is ast.NodeError { 26 | declarations << ast.Raw{ 27 | value: p.lexer.get_lit(err.pos.pos, p.tok.pos().pos) 28 | pos: err.pos.extend(p.tok.pos()) 29 | } 30 | } else { 31 | p.error(err.msg()) 32 | } 33 | } 34 | } 35 | 36 | return declarations 37 | } 38 | 39 | pub fn (mut p Parser) parse_declaration() !ast.Declaration { 40 | defer { 41 | p.is_important = false 42 | } 43 | start_pos := p.tok.pos() 44 | 45 | if p.tok.kind != .name { 46 | // .test { ; } 47 | return p.error('unexpected token, expecting a property name or "}"') 48 | } 49 | 50 | property := p.tok.lit 51 | p.next() 52 | if p.tok.kind != .colon { 53 | // .test { color green; } 54 | return p.error('expecting ":" after a property name') 55 | } 56 | p.next() 57 | 58 | value_pos := p.tok.pos() 59 | values := p.parse_value_children()! 60 | mut value := ast.Value{ 61 | pos: value_pos.extend(p.tok.pos()) 62 | children: values 63 | } 64 | 65 | return ast.Declaration{ 66 | pos: start_pos.extend(p.tok.pos()) 67 | property: property 68 | value: value 69 | important: p.is_important 70 | } 71 | } 72 | 73 | pub fn (mut p Parser) parse_value_children() ![]ast.Node { 74 | mut children := []ast.Node{} 75 | for { 76 | match p.tok.kind { 77 | .exclamation { 78 | p.next() 79 | important_pos := p.prev_tok.pos().extend(p.tok.pos()) 80 | // color: !asdsd; 81 | if p.tok.kind != .name || p.tok.lit != 'important' { 82 | return p.error_with_pos('expecting "!important"', important_pos) 83 | } else { 84 | // color: !important; 85 | if children.len == 0 { 86 | return p.error_with_pos('expecting a value before "!important"', 87 | important_pos) 88 | } 89 | p.is_important = true 90 | // color: red !important green; 91 | if p.peek_tok.kind != .semicolon && p.peek_tok.kind != .rcbr { 92 | return p.error_with_pos('expecting ";": "!important" has to be the last identifier in a declaration', 93 | important_pos) 94 | } 95 | } 96 | } 97 | .number { 98 | // if the tokens are directly next to each other it's a dimension: 100px 99 | // otherwise it's a number and something else: rgb(0 0 0) 100 | if p.tok.pos().pos + p.tok.pos().len == p.peek_tok.pos().pos 101 | && (p.peek_tok.kind == .name || p.peek_tok.kind == .percentage) { 102 | children << p.parse_dimension() 103 | } else { 104 | children << ast.Number{ 105 | pos: p.tok.pos() 106 | value: p.tok.lit 107 | } 108 | } 109 | } 110 | .name { 111 | if p.peek_tok.kind == .lpar { 112 | children << p.parse_function()! 113 | } else { 114 | children << ast.Ident{ 115 | pos: p.tok.pos() 116 | name: p.tok.lit 117 | } 118 | } 119 | } 120 | .string { 121 | // e.g. content: "stuff"; 122 | children << ast.String{ 123 | pos: p.tok.pos() 124 | value: p.tok.lit 125 | } 126 | } 127 | .hash { 128 | hash_start := p.tok.pos() 129 | 130 | color_start := p.peek_tok.pos() 131 | for p.peek_tok.kind in [.name, .number] { 132 | current_pos_end := p.tok.pos().pos + p.tok.pos().len 133 | // break when a space occurs: #fff url() 134 | if current_pos_end != p.peek_tok.pos().pos { 135 | break 136 | } 137 | p.next() 138 | } 139 | 140 | hex_color := p.lexer.get_lit(color_start.pos, p.tok.pos().pos + p.tok.pos().len) 141 | if hex_color.len == 0 { 142 | return p.error('expecting a hex color') 143 | } 144 | 145 | if p.validate_hex_color(hex_color) == false { 146 | // treat the hash element as an ident when inside a function 147 | if p.parentheses_depth != 0 { 148 | children << ast.Hash{ 149 | pos: hash_start.extend(p.tok.pos()) 150 | value: hex_color 151 | } 152 | p.next() 153 | continue 154 | } 155 | return p.error_with_pos('invalid hex color', hash_start.extend(p.tok.pos())) 156 | } 157 | 158 | children << ast.Hash{ 159 | pos: hash_start.extend(p.tok.pos()) 160 | value: hex_color 161 | } 162 | } 163 | // operators 164 | .comma { 165 | children << ast.Operator{ 166 | pos: p.tok.pos() 167 | kind: .comma 168 | } 169 | } 170 | .plus { 171 | children << ast.Operator{ 172 | pos: p.tok.pos() 173 | kind: .plus 174 | } 175 | } 176 | .minus { 177 | children << ast.Operator{ 178 | pos: p.tok.pos() 179 | kind: .min 180 | } 181 | } 182 | .mul { 183 | children << ast.Operator{ 184 | pos: p.tok.pos() 185 | kind: .mul 186 | } 187 | } 188 | .div { 189 | children << ast.Operator{ 190 | pos: p.tok.pos() 191 | kind: .div 192 | } 193 | } 194 | .rcbr { 195 | // it's actually valid css syntax if you don't place a semicolon after the last 196 | // declaration in a block 197 | if p.prefs.is_strict { 198 | return p.error('expecting ";"') 199 | } 200 | if children.len == 0 { 201 | return p.error('property value expected') 202 | } 203 | return children 204 | } 205 | .lpar { 206 | children << p.parse_parentheses()! 207 | } 208 | .rpar { 209 | if p.parentheses_depth <= 0 { 210 | return p.error('unexpected ")" there is no matching opening parenthesis') 211 | } 212 | if children.len == 0 { 213 | return p.error('property value expected') 214 | } 215 | return children 216 | } 217 | .semicolon { 218 | if children.len == 0 { 219 | return p.error('property value expected') 220 | } 221 | if p.parentheses_depth > 0 { 222 | // :not(.test { color: green; } 223 | // ^ 224 | return p.error('unexpected ";" inside a function, expecting ")". Did you forget to close the parentheses?') 225 | } 226 | break 227 | } 228 | else { 229 | return p.unexpected() 230 | } 231 | } 232 | p.next() 233 | } 234 | // skip ';' 235 | p.next() 236 | 237 | return children 238 | } 239 | 240 | pub fn (mut p Parser) parse_dimension() ast.Dimension { 241 | start_pos := p.tok.pos() 242 | value := p.tok.lit 243 | p.next() 244 | unit := p.tok.lit 245 | return ast.Dimension{ 246 | pos: start_pos.extend(p.tok.pos()) 247 | value: value 248 | unit: unit 249 | } 250 | } 251 | 252 | pub fn (mut p Parser) parse_parentheses() !ast.Parentheses { 253 | par_start := p.tok.pos() 254 | p.next() 255 | 256 | p.parentheses_depth++ 257 | children := p.parse_value_children()! 258 | p.parentheses_depth-- 259 | 260 | return ast.Parentheses{ 261 | pos: par_start.extend(p.tok.pos()) 262 | children: children 263 | } 264 | } 265 | 266 | pub fn (mut p Parser) parse_function() !ast.Function { 267 | fn_start := p.tok.pos() 268 | 269 | fn_name := p.tok.lit 270 | // skip "(" 271 | p.next() 272 | p.next() 273 | p.parentheses_depth++ 274 | defer { 275 | p.parentheses_depth-- 276 | } 277 | 278 | children_start := p.tok.pos() 279 | p.mute_unexpected = true 280 | defer { 281 | p.mute_unexpected = false 282 | } 283 | 284 | children := p.parse_value_children() or { 285 | // we can expect "unexpected token" errors in a function, if the error 286 | // wasn't an unexpected token we need to return the error 287 | if err is ast.NodeError { 288 | if err.is_unexpected == false { 289 | return err 290 | } 291 | } 292 | // the error was an unexpected token error, so we convert the chidren 293 | // to a string. This is especially usefull for the `url` function 294 | // e.g. `background-image: url(data:image/png;base64,iRxVB0…);` 295 | if p.tok.kind !in [.rpar, .semicolon] { 296 | for p.tok.kind != .rpar { 297 | p.next() 298 | } 299 | } 300 | 301 | // turn all children into one string if an error occurs 302 | final_pos := children_start.extend(p.prev_tok.pos()) 303 | 304 | return ast.Function{ 305 | pos: fn_start.extend(p.tok.pos()) 306 | name: fn_name 307 | children: [ 308 | ast.String{ 309 | pos: final_pos 310 | value: p.lexer.get_lit(final_pos.pos, final_pos.pos + final_pos.len) 311 | }, 312 | ] 313 | } 314 | } 315 | 316 | if p.tok.kind != .rpar { 317 | return p.error('expecting closing parenthesis ")" not ${p.tok.kind}') 318 | } 319 | return ast.Function{ 320 | pos: fn_start.extend(p.tok.pos()) 321 | name: fn_name 322 | children: children 323 | } 324 | } 325 | 326 | @[inline] 327 | pub fn (mut p Parser) validate_hex_color(val string) bool { 328 | return val.to_lower().contains_only('abcdef0123456789') 329 | } 330 | -------------------------------------------------------------------------------- /css/parser/parser.v: -------------------------------------------------------------------------------- 1 | module parser 2 | 3 | import css.ast 4 | import css.errors 5 | import css.lexer 6 | import css.pref 7 | import css.token 8 | import os 9 | 10 | const double_quote = '"' 11 | 12 | pub struct Parser { 13 | prefs pref.Preferences 14 | mut: 15 | file_name string // "/css/main.css" 16 | tok token.Token 17 | prev_tok token.Token 18 | peek_tok token.Token 19 | inside_pseudo_selector bool 20 | parentheses_depth int 21 | is_pseudo_element bool 22 | is_important bool 23 | // ast_imports // @import() 24 | error_details []string 25 | should_abort bool // when t oo many warnings/errors occur should_abort becomes true and the parser should stop 26 | mute_unexpected bool 27 | pub mut: 28 | table &ast.Table = unsafe { nil } 29 | lexer &lexer.Lexer = unsafe { nil } 30 | has_errored bool 31 | } 32 | 33 | // todo: add options? 34 | pub fn Parser.new(prefs pref.Preferences) &Parser { 35 | return &Parser{ 36 | prefs: prefs 37 | } 38 | } 39 | 40 | // todo: same as above 41 | fn (mut p Parser) init(filename string) { 42 | } 43 | 44 | @[manualfree] 45 | pub fn (mut p Parser) free_lexer() { 46 | unsafe { 47 | if p.lexer != 0 { 48 | p.lexer.free() 49 | p.lexer = &lexer.Lexer(nil) 50 | } 51 | } 52 | } 53 | 54 | pub fn (mut p Parser) set_path(file_path string) { 55 | // TODO: set current directory ? 56 | p.file_name = os.file_name(file_path) 57 | } 58 | 59 | pub fn (mut p Parser) next() { 60 | p.prev_tok = p.tok 61 | p.tok = p.peek_tok 62 | p.peek_tok = p.lexer.scan() 63 | } 64 | 65 | // for testing purposes 66 | pub fn (mut p Parser) parse_text(raw_text string) &ast.StyleSheet { 67 | defer { 68 | unsafe { p.free_lexer() } 69 | } 70 | 71 | p.lexer = &lexer.Lexer{ 72 | prefs: p.prefs 73 | all_tokens: []token.Token{cap: raw_text.len / 3} 74 | text: raw_text 75 | } 76 | p.lexer.scan_remaining_text() 77 | p.lexer.tidx = 0 78 | 79 | return p.parse() 80 | } 81 | 82 | pub fn (mut p Parser) parse_file(path string) &ast.StyleSheet { 83 | defer { 84 | unsafe { p.free_lexer() } 85 | } 86 | $if trace_parser ? { 87 | eprintln('> ${@MOD}.${@FN} path: ${path}') 88 | } 89 | 90 | // p.init(os.file_name(path)) 91 | p.lexer = lexer.new_lexer_file(p.prefs, path) or { panic(err) } 92 | p.set_path(path) 93 | 94 | return p.parse() 95 | } 96 | 97 | pub fn (mut p Parser) parse() &ast.StyleSheet { 98 | // read first token and fill prev_token 99 | p.next() 100 | p.next() 101 | 102 | mut rules := []ast.RuleType{} 103 | 104 | mut imports := []ast.ImportRule{} 105 | 106 | for p.tok.kind == .key_at { 107 | if p.tok.kind == .eof { 108 | break 109 | } 110 | 111 | // only @charset and @layer can come before @import 112 | match p.tok.lit { 113 | // 'import' { 114 | // imports << p.parse_at_import() 115 | // } 116 | 'charset' { 117 | rules << p.parse_at_charset() or { 118 | if err is ast.NodeError { 119 | p.error_with_pos(err.msg(), err.pos) 120 | } 121 | ast.AtRule{} 122 | } 123 | } 124 | 'layer' { 125 | rules << p.parse_at_layer() 126 | } 127 | else { 128 | break 129 | } 130 | } 131 | p.next() 132 | if p.should_abort { 133 | break 134 | } 135 | } 136 | 137 | $if trace_parser ? { 138 | eprintln('> ${@MOD}.${@FN} done parsing import and got ${rules.len} other @rules') 139 | } 140 | 141 | for { 142 | if p.tok.kind == .eof { 143 | // TODO: check unused imports? 144 | break 145 | } 146 | rule := p.top_rule() 147 | rules << rule 148 | if p.should_abort { 149 | break 150 | } 151 | } 152 | 153 | // TODO (scope?) end position 154 | // TOOD: parse text??? 155 | 156 | return &ast.StyleSheet{ 157 | file_path: p.lexer.file_path 158 | imports: imports 159 | rules: rules 160 | } 161 | } 162 | 163 | pub fn (mut p Parser) top_rule() ast.RuleType { 164 | for { 165 | match p.tok.kind { 166 | .key_at { 167 | return p.at_decl() 168 | } 169 | .semicolon { 170 | p.next() 171 | } 172 | else { 173 | return p.block_decl() 174 | } 175 | } 176 | } 177 | // unreachable 178 | return ast.Empty{} 179 | } 180 | 181 | pub fn (mut p Parser) block_decl() ast.Rule { 182 | start_pos := p.tok.pos() 183 | 184 | mut selector_list := []ast.Node{} 185 | for { 186 | if p.tok.kind == .lcbr || p.tok.kind == .eof { 187 | if selector_list.len == 0 { 188 | p.error('expecting a css selector') 189 | } 190 | break 191 | } 192 | 193 | if selector := p.parse_selector() { 194 | if selector.children.len == 0 { 195 | p.error('expecting a css selector') 196 | p.next() 197 | continue 198 | } else { 199 | selector_list << selector 200 | } 201 | } else { 202 | for p.tok.kind != .comma && p.tok.kind != .lcbr { 203 | p.next() 204 | } 205 | if err is ast.NodeError { 206 | p.error_with_pos(err.msg(), err.pos) 207 | // TODO: switch to error node 208 | selector_list << ast.Raw{ 209 | value: p.lexer.get_lit(err.pos.pos, p.tok.pos().pos) 210 | pos: err.pos.extend(p.tok.pos()) 211 | } 212 | } else { 213 | p.error(err.msg()) 214 | } 215 | } 216 | 217 | if p.tok.kind == .lcbr || p.tok.kind == .eof { 218 | break 219 | } else if p.tok.kind != .comma { 220 | // parse error! 221 | p.error('expecting a comma!') 222 | } 223 | p.next() 224 | } 225 | mut prelude := ast.SelectorList{ 226 | pos: start_pos.extend(p.prev_tok.pos()) 227 | children: selector_list 228 | } 229 | 230 | block_start := p.tok.pos() 231 | // skip '{' 232 | p.next() 233 | mut declarations := p.parse_declaration_block() 234 | 235 | mut rule := ast.Rule{ 236 | pos: start_pos.extend(p.tok.pos()) 237 | prelude: prelude 238 | block: ast.Block{ 239 | pos: block_start.extend(p.tok.pos()) 240 | declarations: declarations 241 | } 242 | } 243 | // skip '}' 244 | p.next() 245 | 246 | // p.table.insert_rule(prelude.children, declarations) 247 | 248 | return rule 249 | } 250 | 251 | pub fn (mut p Parser) pseudo_decl() ast.Rule { 252 | return ast.Rule{} 253 | } 254 | 255 | pub fn (mut p Parser) recover_until(recover_token token.Kind) { 256 | for p.tok.kind != recover_token && p.tok.kind != .eof { 257 | p.next() 258 | } 259 | } 260 | 261 | pub fn (mut p Parser) recover_until_arr(recover_tokens ...token.Kind) { 262 | for p.tok.kind !in recover_tokens && p.tok.kind != .eof { 263 | p.next() 264 | } 265 | } 266 | 267 | // skip block finds the next '{' and continues until that brace is closed 268 | pub fn (mut p Parser) skip_block() { 269 | mut brace_count := 0 270 | mut started := false 271 | for { 272 | if p.should_abort { 273 | return 274 | } 275 | match p.tok.kind { 276 | .eof { 277 | break 278 | } 279 | .lcbr { 280 | started = true 281 | brace_count++ 282 | } 283 | .rcbr { 284 | if started == true { 285 | brace_count-- 286 | if brace_count <= 0 { 287 | break 288 | } 289 | } 290 | } 291 | else {} 292 | } 293 | p.next() 294 | } 295 | p.next() 296 | } 297 | 298 | @[params] 299 | struct ParamsForUnexpected { 300 | got string 301 | expecting string 302 | prepend_msg string 303 | additional_msg string 304 | } 305 | 306 | fn (mut p Parser) unexpected(params ParamsForUnexpected) ast.NodeError { 307 | return p.unexpected_with_pos(p.tok.pos(), params) 308 | } 309 | 310 | fn (mut p Parser) unexpected_with_pos(pos token.Pos, params ParamsForUnexpected) ast.NodeError { 311 | mut msg := if params.got != '' { 312 | 'unexpected token ${params.got}' 313 | } else { 314 | 'unexpected token ${p.tok}' 315 | } 316 | if params.expecting != '' { 317 | msg += ', expecting ${params.expecting}' 318 | } 319 | if params.prepend_msg != '' { 320 | msg = '${params.prepend_msg} ' + msg 321 | } 322 | if params.additional_msg != '' { 323 | msg += ', ${params.additional_msg}' 324 | } 325 | 326 | if p.mute_unexpected { 327 | return ast.NodeError{ 328 | pos: pos 329 | msg: msg 330 | is_unexpected: true 331 | } 332 | } else { 333 | return p.error_with_pos(msg, pos) 334 | } 335 | } 336 | 337 | pub fn (mut p Parser) warn(msg string) { 338 | p.warn_with_pos(msg, p.tok.pos()) 339 | } 340 | 341 | pub fn (mut p Parser) get_details() string { 342 | mut details := '' 343 | if p.error_details.len > 0 { 344 | details = '\n' + p.error_details.join('\n') 345 | p.error_details = [] 346 | } 347 | return details 348 | } 349 | 350 | pub fn (mut p Parser) warn_with_pos(msg string, pos token.Pos) { 351 | details := p.get_details() 352 | if !p.prefs.suppress_output { 353 | errors.show_compiler_message('warning:', 354 | msg: msg 355 | details: details 356 | file_path: p.lexer.file_path 357 | pos: pos 358 | ) 359 | } 360 | } 361 | 362 | pub fn (mut p Parser) error(msg string) ast.NodeError { 363 | return p.error_with_pos(msg, p.tok.pos()) 364 | } 365 | 366 | pub fn (mut p Parser) error_with_pos(msg string, pos token.Pos) ast.NodeError { 367 | p.has_errored = true 368 | details := p.get_details() 369 | 370 | if !p.prefs.suppress_output { 371 | errors.show_compiler_message('error:', 372 | msg: msg 373 | details: details 374 | file_path: p.lexer.file_path 375 | pos: pos 376 | ) 377 | } 378 | 379 | return ast.NodeError{ 380 | pos: pos 381 | msg: msg 382 | } 383 | } 384 | -------------------------------------------------------------------------------- /css/parser/parser_bootstrap_test.v: -------------------------------------------------------------------------------- 1 | import css.ast 2 | import css.parser 3 | import css.pref 4 | 5 | const css_file = '${@VMODROOT}/tests/testdata/bootstrap.css' 6 | const min_css_file = '${@VMODROOT}/tests/testdata/bootstrap.min.css' 7 | 8 | fn test_no_errors() { 9 | mut p := parser.Parser.new(pref.Preferences{ 10 | suppress_output: true 11 | is_strict: true 12 | }) 13 | p.table = &ast.Table{} 14 | p.parse_file(css_file) 15 | 16 | assert p.has_errored == false 17 | } 18 | 19 | fn test_no_errors_minified() { 20 | mut p := parser.Parser.new(pref.Preferences{ 21 | suppress_output: true 22 | }) 23 | p.table = &ast.Table{} 24 | p.parse_file(min_css_file) 25 | 26 | assert p.has_errored == false 27 | } 28 | -------------------------------------------------------------------------------- /css/parser/selector.v: -------------------------------------------------------------------------------- 1 | module parser 2 | 3 | import css.ast 4 | import css.token 5 | 6 | pub fn (mut p Parser) parse_selector() !ast.Selector { 7 | mut selector := ast.Selector{ 8 | pos: p.tok.pos() 9 | } 10 | 11 | for { 12 | start_pos := p.tok.pos() 13 | 14 | // check for difference between `.other .test` and `.other.test` 15 | // if the position differs one or more places that means a space is between the selectors 16 | // other tokens should throw errors 17 | prev_pos := p.prev_tok.pos() 18 | mut should_have_descenent_selector := start_pos.pos - (prev_pos.pos + prev_pos.len) > 0 19 | && selector.children.len > 0 && selector.children.last() !is ast.Combinator 20 | 21 | match p.tok.kind { 22 | .dot { 23 | p.next() 24 | if p.tok.kind != .name { 25 | // . {} 26 | return ast.NodeError{ 27 | pos: start_pos.extend(p.tok.pos()) 28 | msg: 'expecting an identifier after "."' 29 | } 30 | } 31 | selector.children << ast.ClassSelector{ 32 | pos: start_pos.extend(p.tok.pos()) 33 | name: p.tok.lit 34 | } 35 | } 36 | .hash { 37 | p.next() 38 | if p.tok.kind != .name { 39 | // color: #; 40 | return ast.NodeError{ 41 | pos: start_pos.extend(p.tok.pos()) 42 | msg: 'expecting an identifier after "#" in a selector' 43 | } 44 | } 45 | selector.children << ast.IdSelector{ 46 | pos: start_pos.extend(p.tok.pos()) 47 | name: p.tok.lit 48 | } 49 | } 50 | .name { 51 | selector.children << ast.TypeSelector{ 52 | pos: start_pos.extend(p.tok.pos()) 53 | name: p.tok.lit 54 | } 55 | } 56 | .mul { 57 | selector.children << ast.TypeSelector{ 58 | pos: start_pos.extend(p.tok.pos()) 59 | name: '*' 60 | } 61 | } 62 | .colon { 63 | // if p.inside_pseudo_selector { 64 | // p.unexpected(got: 'token ${p.tok.kind}') 65 | // return ast.NodeError{ 66 | // pos: start_pos.extend(p.tok.pos()) 67 | // msg: 'You can not have a pseudo selector inside of a pseudo selector' 68 | // } 69 | // } 70 | 71 | p.next() 72 | if p.tok.kind == .colon || p.tok.kind == .name { 73 | // : 74 | if p.tok.kind == .colon { 75 | // :: 76 | p.is_pseudo_element = true 77 | p.next() 78 | } 79 | selector.children << p.parse_pseudo_selector() or { 80 | p.is_pseudo_element = false 81 | return err 82 | } 83 | p.is_pseudo_element = false 84 | } else { 85 | // .test: {} 86 | return ast.NodeError{ 87 | pos: start_pos.extend(p.tok.pos()) 88 | msg: 'expecting an identifier or ":" after ":" in a selector' 89 | } 90 | } 91 | } 92 | .rpar { 93 | if p.inside_pseudo_selector { 94 | break 95 | } else { 96 | // :not.test) 97 | return ast.NodeError{ 98 | pos: start_pos.extend(p.tok.pos()) 99 | msg: 'expecting a "(" before ")" inside a selector' 100 | } 101 | } 102 | } 103 | .lsbr { 104 | if p.peek_tok.kind != .name { 105 | // a[] {} 106 | return ast.NodeError{ 107 | pos: start_pos.extend(p.tok.pos()) 108 | msg: 'expecting an identifier' 109 | } 110 | } 111 | 112 | selector.children << p.parse_attr()! 113 | } 114 | // combinators 115 | .gt { 116 | should_have_descenent_selector = false 117 | selector.children << ast.Combinator{ 118 | pos: start_pos.extend(p.tok.pos()) 119 | kind: '>' 120 | } 121 | } 122 | .plus { 123 | should_have_descenent_selector = false 124 | selector.children << ast.Combinator{ 125 | pos: start_pos.extend(p.tok.pos()) 126 | kind: '+' 127 | } 128 | } 129 | else { 130 | break 131 | } 132 | } 133 | 134 | if should_have_descenent_selector { 135 | // insert combinator in between `.test .other` 136 | selector.children.insert(selector.children.len - 1, ast.Combinator{ 137 | pos: token.Pos{ 138 | len: start_pos.pos - (prev_pos.pos + prev_pos.len) 139 | pos: prev_pos.pos + prev_pos.len 140 | col: prev_pos.col + prev_pos.len 141 | } 142 | kind: ' ' 143 | }) 144 | } 145 | 146 | p.next() 147 | } 148 | 149 | selector.pos.extend(p.tok.pos()) 150 | return selector 151 | } 152 | 153 | pub fn (mut p Parser) parse_pseudo_selector() !ast.PseudoSelector { 154 | pseudo_start := p.tok.pos() 155 | 156 | if p.tok.kind != .name { 157 | // .test: {} 158 | return ast.NodeError{ 159 | pos: pseudo_start.extend(p.tok.pos()) 160 | msg: 'expecting a name after a pseudo class selector ":"' 161 | } 162 | } 163 | pseudo_selector_name := p.tok.lit 164 | 165 | // :not() 166 | if p.peek_tok.kind == .lpar { 167 | if p.inside_pseudo_selector { 168 | // :not(.test:has(.other)) 169 | return ast.NodeError{ 170 | pos: p.tok.pos() 171 | msg: 'You can not have a pseudo selector function inside of a pseudo selector function' 172 | } 173 | } 174 | 175 | p.next() 176 | p.next() 177 | 178 | p.inside_pseudo_selector = true 179 | sub_selector := p.parse_selector() or { 180 | p.inside_pseudo_selector = false 181 | 182 | // continue until ")" 183 | for p.tok.kind != .rpar { 184 | p.next() 185 | } 186 | 187 | if err is ast.NodeError { 188 | p.error_with_pos(err.msg(), err.pos) 189 | return ast.Raw{ 190 | value: p.lexer.get_lit(err.pos.pos, p.tok.pos().pos) 191 | pos: err.pos.extend(p.tok.pos()) 192 | } 193 | } 194 | 195 | return err 196 | } 197 | p.inside_pseudo_selector = false 198 | if p.tok.kind != .rpar { 199 | // :not( { 200 | return ast.NodeError{ 201 | pos: p.tok.pos() 202 | msg: 'expecting a closing parenthesis ")" after a pseudo selector' 203 | } 204 | } 205 | 206 | if p.is_pseudo_element { 207 | return ast.PseudoElementSelector{ 208 | pos: pseudo_start.extend(p.tok.pos()) 209 | name: pseudo_selector_name 210 | children: sub_selector.children 211 | } 212 | } else { 213 | return ast.PseudoClassSelector{ 214 | pos: pseudo_start.extend(p.tok.pos()) 215 | name: pseudo_selector_name 216 | children: sub_selector.children 217 | } 218 | } 219 | } else { 220 | // :first-child 221 | if p.is_pseudo_element { 222 | return ast.PseudoElementSelector{ 223 | pos: pseudo_start.extend(p.tok.pos()) 224 | name: pseudo_selector_name 225 | } 226 | } else { 227 | return ast.PseudoClassSelector{ 228 | pos: pseudo_start.extend(p.tok.pos()) 229 | name: pseudo_selector_name 230 | } 231 | } 232 | } 233 | } 234 | 235 | pub fn (mut p Parser) parse_attr() !ast.AttributeSelector { 236 | attr_start := p.tok.pos() 237 | 238 | p.next() 239 | attr_name := ast.Ident{ 240 | pos: p.tok.pos() 241 | name: p.tok.lit 242 | } 243 | p.next() 244 | 245 | mut should_check_equals := true 246 | attr_match_type := match p.tok.kind { 247 | .equal { 248 | // a[href="#test"] 249 | should_check_equals = false 250 | ast.AttributeMatchType.exact 251 | } 252 | .carrot { 253 | // a[href^="#test"] 254 | ast.AttributeMatchType.starts_with 255 | } 256 | .mul { 257 | // a[href*="#test"] 258 | ast.AttributeMatchType.contains 259 | } 260 | .dollar { 261 | // a[href$="#test"] 262 | ast.AttributeMatchType.ends_with 263 | } 264 | .rsbr { 265 | // a[href] 266 | return ast.AttributeSelector{ 267 | pos: attr_start.extend(p.tok.pos()) 268 | name: attr_name 269 | } 270 | } 271 | else { 272 | return ast.NodeError{ 273 | pos: attr_start.extend(p.tok.pos()) 274 | msg: 'unexpected token' 275 | } 276 | } 277 | } 278 | 279 | if should_check_equals { 280 | p.next() 281 | if p.tok.kind != .equal { 282 | // a[href^test] 283 | return ast.NodeError{ 284 | pos: p.tok.pos() 285 | msg: 'expecting "=" after the attribute match type' 286 | } 287 | } 288 | } 289 | 290 | p.next() 291 | if p.tok.kind != .string { 292 | if p.tok.kind != .name || p.prefs.is_strict { 293 | // a[href=#test] 294 | return ast.NodeError{ 295 | pos: p.tok.pos() 296 | msg: 'expected a string after "=" inside the attribute selector' 297 | } 298 | } 299 | // else { 300 | // p.warn('expected a string after "=" inside the attribute selector') 301 | // } 302 | } 303 | attr_value := p.tok.lit 304 | p.next() 305 | if p.tok.kind != .rsbr { 306 | // a[href="#test" {} 307 | return ast.NodeError{ 308 | pos: p.tok.pos() 309 | msg: 'expecting a "]" to close the attribute selector' 310 | } 311 | } 312 | 313 | return ast.AttributeSelector{ 314 | pos: attr_start.extend(p.tok.pos()) 315 | name: attr_name 316 | matcher: attr_match_type 317 | value: ast.String{ 318 | pos: p.tok.pos() 319 | value: attr_value 320 | } 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /css/parser/tests/attr_selector.css: -------------------------------------------------------------------------------- 1 | a[] {} 2 | 3 | a[href^"test"] {} 4 | 5 | a[href=test] {} 6 | 7 | a[href="#test" {} -------------------------------------------------------------------------------- /css/parser/tests/attr_selector.out: -------------------------------------------------------------------------------- 1 | css/parser/tests/attr_selector.css:1:2: error: expecting an identifier 2 | 1 | a[] {} 3 | | ^ 4 | 2 | 5 | 3 | a[href^"test"] {} 6 | css/parser/tests/attr_selector.css:3:8: error: expecting "=" after the attribute match type 7 | 1 | a[] {} 8 | 2 | 9 | 3 | a[href^"test"] {} 10 | | ~~~~~~ 11 | 4 | 12 | 5 | a[href=test] {} 13 | css/parser/tests/attr_selector.css:7:16: error: expecting a "]" to close the attribute selector 14 | 5 | a[href=test] {} 15 | 6 | 16 | 7 | a[href="#test" {} 17 | | ^ -------------------------------------------------------------------------------- /css/parser/tests/attr_selector_strict.css: -------------------------------------------------------------------------------- 1 | a[href=test] {} -------------------------------------------------------------------------------- /css/parser/tests/attr_selector_strict.out: -------------------------------------------------------------------------------- 1 | css/parser/tests/attr_selector_strict.css:1:8: error: expected a string after "=" inside the attribute selector 2 | 1 | a[href=test] {} 3 | | ~~~~ -------------------------------------------------------------------------------- /css/parser/tests/charset.css: -------------------------------------------------------------------------------- 1 | 2 | @charset "UTF-8"; 3 | 4 | @charset 'utf-8'; 5 | 6 | @charset "ISO-12345" ; 7 | 8 | @charset "ASCII" 9 | 10 | .test {} 11 | -------------------------------------------------------------------------------- /css/parser/tests/charset.out: -------------------------------------------------------------------------------- 1 | css/parser/tests/charset.css:4:10: error: invalid charset rule: expecting a double quoted string after "@charset" 2 | 2 | @charset "UTF-8"; 3 | 3 | 4 | 4 | @charset 'utf-8'; 5 | | ~~~~~~~ 6 | 5 | 7 | 6 | @charset "ISO-12345" ; 8 | css/parser/tests/charset.css:4:10: error: invalid charset rule: expecting a double quoted string after "@charset" 9 | 2 | @charset "UTF-8"; 10 | 3 | 11 | 4 | @charset 'utf-8'; 12 | | ~~~~~~~ 13 | 5 | 14 | 6 | @charset "ISO-12345" ; 15 | css/parser/tests/charset.css:6:25: error: the ";" has to be the next character after "ISO-12345" 16 | 4 | @charset 'utf-8'; 17 | 5 | 18 | 6 | @charset "ISO-12345" ; 19 | | ^ 20 | 7 | 21 | 8 | @charset "ASCII" 22 | css/parser/tests/charset.css:10:1: error: expecting a semicolon to close the at-rule 23 | 8 | @charset "ASCII" 24 | 9 | 25 | 10 | .test {} 26 | | ^ -------------------------------------------------------------------------------- /css/parser/tests/continue_on_single_missing_parentheses.css: -------------------------------------------------------------------------------- 1 | .test { 2 | width: calc(100vw - (20px * 30px); 3 | color: rgba( 0 0 0; 4 | width: calc(100vw - (20px * 30px)); 5 | } -------------------------------------------------------------------------------- /css/parser/tests/continue_on_single_missing_parentheses.out: -------------------------------------------------------------------------------- 1 | css/parser/tests/continue_on_single_missing_parentheses.css:2:38: error: unexpected ";" inside a function, expecting ")". Did you forget to close the parentheses? 2 | 1 | .test { 3 | 2 | width: calc(100vw - (20px * 30px); 4 | | ^ 5 | 3 | color: rgba( 0 0 0; 6 | 4 | width: calc(100vw - (20px * 30px)); 7 | css/parser/tests/continue_on_single_missing_parentheses.css:3:23: error: unexpected ";" inside a function, expecting ")". Did you forget to close the parentheses? 8 | 1 | .test { 9 | 2 | width: calc(100vw - (20px * 30px); 10 | 3 | color: rgba( 0 0 0; 11 | | ^ 12 | 4 | width: calc(100vw - (20px * 30px)); 13 | 5 | } -------------------------------------------------------------------------------- /css/parser/tests/declaration.css: -------------------------------------------------------------------------------- 1 | 2 | .test { 3 | ; 4 | color green; 5 | color: rgba 0 0 0); 6 | } 7 | 8 | .test { 9 | color: red; 10 | } 11 | -------------------------------------------------------------------------------- /css/parser/tests/declaration.out: -------------------------------------------------------------------------------- 1 | css/parser/tests/declaration.css:3:5: error: unexpected token, expecting a property name or "}" 2 | 1 | 3 | 2 | .test { 4 | 3 | ; 5 | | ^ 6 | 4 | color green; 7 | 5 | color: rgba 0 0 0); 8 | css/parser/tests/declaration.css:4:11: error: expecting ":" after a property name 9 | 2 | .test { 10 | 3 | ; 11 | 4 | color green; 12 | | ~~~~~ 13 | 5 | color: rgba 0 0 0); 14 | 6 | } 15 | css/parser/tests/declaration.css:5:22: error: unexpected ")" there is no matching opening parenthesis 16 | 3 | ; 17 | 4 | color green; 18 | 5 | color: rgba 0 0 0); 19 | | ^ 20 | 6 | } 21 | 7 | -------------------------------------------------------------------------------- /css/parser/tests/empty_at_rule.css: -------------------------------------------------------------------------------- 1 | 2 | .test { 3 | color: green; 4 | } 5 | 6 | @ -------------------------------------------------------------------------------- /css/parser/tests/empty_at_rule.out: -------------------------------------------------------------------------------- 1 | css/parser/tests/empty_at_rule.css:6:2: error: expecting a name after "@" 2 | 4 | } 3 | 5 | 4 | 6 | @ 5 | | ^ -------------------------------------------------------------------------------- /css/parser/tests/empty_declaration.css: -------------------------------------------------------------------------------- 1 | .empty { 2 | color: ; 3 | } -------------------------------------------------------------------------------- /css/parser/tests/empty_declaration.out: -------------------------------------------------------------------------------- 1 | css/parser/tests/empty_declaration.css:2:12: error: property value expected 2 | 1 | .empty { 3 | 2 | color: ; 4 | | ^ 5 | 3 | } -------------------------------------------------------------------------------- /css/parser/tests/empty_selector.css: -------------------------------------------------------------------------------- 1 | { 2 | color: green; 3 | } -------------------------------------------------------------------------------- /css/parser/tests/empty_selector.out: -------------------------------------------------------------------------------- 1 | css/parser/tests/empty_selector.css:1:1: error: expecting a css selector 2 | 1 | { 3 | | ^ 4 | 2 | color: green; 5 | 3 | } -------------------------------------------------------------------------------- /css/parser/tests/hex_colors.css: -------------------------------------------------------------------------------- 1 | 2 | .dim { 3 | color: #; 4 | color: #abcdef; 5 | color: #123456; 6 | color: #7890j0; 7 | color: #zzxxzz; 8 | } -------------------------------------------------------------------------------- /css/parser/tests/hex_colors.out: -------------------------------------------------------------------------------- 1 | css/parser/tests/hex_colors.css:3:12: error: expecting a hex color 2 | 1 | 3 | 2 | .dim { 4 | 3 | color: #; 5 | | ^ 6 | 4 | color: #abcdef; 7 | 5 | color: #123456; 8 | css/parser/tests/hex_colors.css:6:12: error: invalid hex color 9 | 4 | color: #abcdef; 10 | 5 | color: #123456; 11 | 6 | color: #7890j0; 12 | | ~~~~~~~ 13 | 7 | color: #zzxxzz; 14 | 8 | } 15 | css/parser/tests/hex_colors.css:7:12: error: invalid hex color 16 | 5 | color: #123456; 17 | 6 | color: #7890j0; 18 | 7 | color: #zzxxzz; 19 | | ~~~~~~~ 20 | 8 | } -------------------------------------------------------------------------------- /css/parser/tests/important.css: -------------------------------------------------------------------------------- 1 | .test { 2 | color: !asdf; 3 | background: !important; 4 | border: black 1px !important solid; 5 | padding: 10px !important; 6 | } -------------------------------------------------------------------------------- /css/parser/tests/important.out: -------------------------------------------------------------------------------- 1 | css/parser/tests/important.css:2:12: error: expecting "!important" 2 | 1 | .test { 3 | 2 | color: !asdf; 4 | | ~~~~~ 5 | 3 | background: !important; 6 | 4 | border: black 1px !important solid; 7 | css/parser/tests/important.css:3:17: error: expecting a value before "!important" 8 | 1 | .test { 9 | 2 | color: !asdf; 10 | 3 | background: !important; 11 | | ~~~~~~~~~~ 12 | 4 | border: black 1px !important solid; 13 | 5 | padding: 10px !important; 14 | css/parser/tests/important.css:4:23: error: expecting ";": "!important" has to be the last identifier in a declaration 15 | 2 | color: !asdf; 16 | 3 | background: !important; 17 | 4 | border: black 1px !important solid; 18 | | ~~~~~~~~~~ 19 | 5 | padding: 10px !important; 20 | 6 | } -------------------------------------------------------------------------------- /css/parser/tests/keyframes.css: -------------------------------------------------------------------------------- 1 | 2 | @keyframes {} 3 | 4 | @keyframes test } 5 | 6 | .test { 7 | color: red; 8 | } 9 | 10 | @keyframes next { 11 | from 12 | to {} 13 | not_from {} 14 | 10% 15 | 20% {} 16 | 60% {} 17 | .test { 18 | 19 | } 20 | } 21 | 22 | .color { 23 | background-color: red; 24 | } 25 | 26 | .color2 { 27 | background-color: red; 28 | } -------------------------------------------------------------------------------- /css/parser/tests/keyframes.out: -------------------------------------------------------------------------------- 1 | css/parser/tests/keyframes.css:2:12: error: expecting the name of the keyframes rule 2 | 1 | 3 | 2 | @keyframes {} 4 | | ^ 5 | 3 | 6 | 4 | @keyframes test } 7 | css/parser/tests/keyframes.css:4:17: error: expecting "{" 8 | 2 | @keyframes {} 9 | 3 | 10 | 4 | @keyframes test } 11 | | ^ 12 | 5 | 13 | 6 | .test { 14 | css/parser/tests/keyframes.css:12:5: error: expecting "{" 15 | 10 | @keyframes next { 16 | 11 | from 17 | 12 | to {} 18 | | ~~ 19 | 13 | not_from {} 20 | 14 | 10% 21 | css/parser/tests/keyframes.css:13:5: error: expecting a percentage or "from" or "to" 22 | 11 | from 23 | 12 | to {} 24 | 13 | not_from {} 25 | | ~~~~~~~~ 26 | 14 | 10% 27 | 15 | 20% {} 28 | css/parser/tests/keyframes.css:15:5: error: expecting "{" 29 | 13 | not_from {} 30 | 14 | 10% 31 | 15 | 20% {} 32 | | ~~ 33 | 16 | 60% {} 34 | 17 | .test { 35 | css/parser/tests/keyframes.css:17:5: error: expecting a keyframes rule 36 | 15 | 20% {} 37 | 16 | 60% {} 38 | 17 | .test { 39 | | ^ 40 | 18 | 41 | 19 | } 42 | css/parser/tests/keyframes.css:20:1: error: expecting a css selector 43 | 18 | 44 | 19 | } 45 | 20 | } 46 | | ^ 47 | 21 | 48 | 22 | .color { -------------------------------------------------------------------------------- /css/parser/tests/last_declaration_without_semi.css: -------------------------------------------------------------------------------- 1 | 2 | .test { 3 | color: green 4 | } -------------------------------------------------------------------------------- /css/parser/tests/last_declaration_without_semi.out: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Casper64/css/404d52181f09ebf0a172479d47508f599ce58115/css/parser/tests/last_declaration_without_semi.out -------------------------------------------------------------------------------- /css/parser/tests/last_declaration_without_semi_strict.css: -------------------------------------------------------------------------------- 1 | 2 | .test { 3 | color: green 4 | } -------------------------------------------------------------------------------- /css/parser/tests/last_declaration_without_semi_strict.out: -------------------------------------------------------------------------------- 1 | css/parser/tests/last_declaration_without_semi_strict.css:4:1: error: expecting ";" 2 | 2 | .test { 3 | 3 | color: green 4 | 4 | } 5 | | ^ -------------------------------------------------------------------------------- /css/parser/tests/layer.css: -------------------------------------------------------------------------------- 1 | 2 | @layer test other; 3 | 4 | @layer test, , other; 5 | 6 | @layer test { 7 | .test { 8 | color: green; 9 | } 10 | -------------------------------------------------------------------------------- /css/parser/tests/layer.out: -------------------------------------------------------------------------------- 1 | css/parser/tests/layer.css:2:13: warning: expecting a comma separated list. Missing "," before "other" 2 | 1 | 3 | 2 | @layer test other; 4 | | ~~~~~ 5 | 3 | 6 | 4 | @layer test, , other; 7 | css/parser/tests/layer.css:4:14: warning: expecting a name, ";" or "{" not "," 8 | 2 | @layer test other; 9 | 3 | 10 | 4 | @layer test, , other; 11 | | ^ 12 | 5 | 13 | 6 | @layer test { 14 | css/parser/tests/layer.css:10:1: error: expecting "}". Did you forget to close the previous block? 15 | 8 | color: green; 16 | 9 | } -------------------------------------------------------------------------------- /css/parser/tests/media.css: -------------------------------------------------------------------------------- 1 | @media only, ,print, {} 2 | 3 | @media (max-width: 600px) { 4 | .test { 5 | color: green; 6 | } 7 | 8 | .other { 9 | color: red; 10 | } 11 | 12 | -------------------------------------------------------------------------------- /css/parser/tests/media.out: -------------------------------------------------------------------------------- 1 | css/parser/tests/media.css:1:14: warning: expecting a media query or "{" 2 | 1 | @media only, ,print, {} 3 | | ^ 4 | 2 | 5 | 3 | @media (max-width: 600px) { 6 | css/parser/tests/media.css:1:20: warning: expecting a media query or "{" 7 | 1 | @media only, ,print, {} 8 | | ^ 9 | 2 | 10 | 3 | @media (max-width: 600px) { 11 | css/parser/tests/media.css:12:1: error: expecting "}". Did you forget to close the previous block? 12 | 10 | } 13 | 11 | -------------------------------------------------------------------------------- /css/parser/tests/media_feature.css: -------------------------------------------------------------------------------- 1 | @media (min-width: ) {} 2 | 3 | @media (min-width: 600px 400px) {} 4 | 5 | @media (min-width: df hover) {} 6 | 7 | @media (min-width: df : hover) {} 8 | -------------------------------------------------------------------------------- /css/parser/tests/media_feature.out: -------------------------------------------------------------------------------- 1 | css/parser/tests/media_feature.css:1:20: error: unexpected token: expecting a term 2 | 1 | @media (min-width: ) {} 3 | | ^ 4 | 2 | 5 | 3 | @media (min-width: 600px 400px) {} 6 | css/parser/tests/media_feature.css:3:27: error: unexpected token: expecting ")" 7 | 1 | @media (min-width: ) {} 8 | 2 | 9 | 3 | @media (min-width: 600px 400px) {} 10 | | ~~~ 11 | 4 | 12 | 5 | @media (min-width: df hover) {} 13 | css/parser/tests/media_feature.css:3:32: error: unexpected token token rpar 14 | 1 | @media (min-width: ) {} 15 | 2 | 16 | 3 | @media (min-width: 600px 400px) {} 17 | | ^ 18 | 4 | 19 | 5 | @media (min-width: df hover) {} 20 | css/parser/tests/media_feature.css:5:24: error: unexpected token: expecting ")" 21 | 3 | @media (min-width: 600px 400px) {} 22 | 4 | 23 | 5 | @media (min-width: df hover) {} 24 | | ~~~~~ 25 | 6 | 26 | 7 | @media (min-width: df : hover) {} 27 | css/parser/tests/media_feature.css:5:29: error: unexpected token token rpar 28 | 3 | @media (min-width: 600px 400px) {} 29 | 4 | 30 | 5 | @media (min-width: df hover) {} 31 | | ^ 32 | 6 | 33 | 7 | @media (min-width: df : hover) {} 34 | css/parser/tests/media_feature.css:7:23: error: unexpected token: expecting ")" 35 | 5 | @media (min-width: df hover) {} 36 | 6 | 37 | 7 | @media (min-width: df : hover) {} 38 | | ^ 39 | css/parser/tests/media_feature.css:7:30: error: unexpected token token rpar 40 | 5 | @media (min-width: df hover) {} 41 | 6 | 42 | 7 | @media (min-width: df : hover) {} 43 | | ^ -------------------------------------------------------------------------------- /css/parser/tests/missing_selector_ident.css: -------------------------------------------------------------------------------- 1 | . { 2 | color: red; 3 | } 4 | 5 | # { 6 | color: white; 7 | } 8 | 9 | .test: { 10 | color: blue 11 | } -------------------------------------------------------------------------------- /css/parser/tests/missing_selector_ident.out: -------------------------------------------------------------------------------- 1 | css/parser/tests/missing_selector_ident.css:1:1: error: expecting an identifier after "." 2 | 1 | . { 3 | | ~~~ 4 | 2 | color: red; 5 | 3 | } 6 | css/parser/tests/missing_selector_ident.css:5:1: error: expecting an identifier after "#" in a selector 7 | 3 | } 8 | 4 | 9 | 5 | # { 10 | | ~~~ 11 | 6 | color: white; 12 | 7 | } 13 | css/parser/tests/missing_selector_ident.css:9:6: error: expecting an identifier or ":" after ":" in a selector 14 | 7 | } 15 | 8 | 16 | 9 | .test: { 17 | | ~~~ 18 | 10 | color: blue 19 | 11 | } -------------------------------------------------------------------------------- /css/parser/tests/pseudo_selector.css: -------------------------------------------------------------------------------- 1 | 2 | a:not.test) { 3 | color: red; 4 | } 5 | 6 | .test: { 7 | color: white; 8 | } 9 | 10 | p:not(.test:has(.other)) { 11 | color: blue; 12 | } 13 | 14 | .test:not(.test { 15 | 16 | } -------------------------------------------------------------------------------- /css/parser/tests/pseudo_selector.out: -------------------------------------------------------------------------------- 1 | css/parser/tests/pseudo_selector.css:2:11: error: expecting a "(" before ")" inside a selector 2 | 1 | 3 | 2 | a:not.test) { 4 | | ^ 5 | 3 | color: red; 6 | 4 | } 7 | css/parser/tests/pseudo_selector.css:6:6: error: expecting an identifier or ":" after ":" in a selector 8 | 4 | } 9 | 5 | 10 | 6 | .test: { 11 | | ~~~ 12 | 7 | color: white; 13 | 8 | } 14 | css/parser/tests/pseudo_selector.css:10:13: error: You can not have a pseudo selector function inside of a pseudo selector function 15 | 8 | } 16 | 9 | 17 | 10 | p:not(.test:has(.other)) { 18 | | ~~~ 19 | 11 | color: blue; 20 | 12 | } 21 | css/parser/tests/pseudo_selector.css:10:24: error: expecting a "(" before ")" inside a selector 22 | 8 | } 23 | 9 | 24 | 10 | p:not(.test:has(.other)) { 25 | | ^ 26 | 11 | color: blue; 27 | 12 | } 28 | css/parser/tests/pseudo_selector.css:14:17: error: expecting a closing parenthesis ")" after a pseudo selector 29 | 12 | } 30 | 13 | 31 | 14 | .test:not(.test { 32 | | ^ 33 | 15 | 34 | 16 | } -------------------------------------------------------------------------------- /css/parser/tests/raw_url.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .no_errors { 4 | background-image: url(https://example.com/images/myImg.jpg); 5 | background-image: url(data:image/png;base64,iRxVB0…); 6 | background-image: url(myFont.woff); 7 | background-image: url(#IDofSVGpath); 8 | } -------------------------------------------------------------------------------- /css/parser/tests/raw_url.out: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Casper64/css/404d52181f09ebf0a172479d47508f599ce58115/css/parser/tests/raw_url.out -------------------------------------------------------------------------------- /css/parser/tests/test_program.v: -------------------------------------------------------------------------------- 1 | import css 2 | import css.ast 3 | import css.parser 4 | import css.pref 5 | import os 6 | 7 | fn main() { 8 | if os.args.len < 2 { 9 | panic('usage: v run main.v CSS_FILE [--strict]') 10 | } 11 | 12 | is_strict := '--strict' in os.args 13 | mut prefs := pref.Preferences{ 14 | is_strict: is_strict 15 | } 16 | 17 | mut p := parser.Parser.new(prefs) 18 | p.table = &ast.Table{} 19 | p.parse_file(os.args[1]) 20 | } 21 | -------------------------------------------------------------------------------- /css/parser/tests/unkown_at_rule.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | @bleh {} -------------------------------------------------------------------------------- /css/parser/tests/unkown_at_rule.out: -------------------------------------------------------------------------------- 1 | css/parser/tests/unkown_at_rule.css:3:2: warning: unkown at-rule "bleh" 2 | 1 | 3 | 2 | 4 | 3 | @bleh {} 5 | | ~~~~ -------------------------------------------------------------------------------- /css/pref/pref.v: -------------------------------------------------------------------------------- 1 | module pref 2 | 3 | @[params] 4 | pub struct Preferences { 5 | pub mut: 6 | is_strict bool 7 | suppress_output bool 8 | } 9 | -------------------------------------------------------------------------------- /css/properties.v: -------------------------------------------------------------------------------- 1 | module css 2 | 3 | import css.datatypes 4 | 5 | const zero_px = datatypes.Length{ 6 | amount: 0 7 | unit: .px 8 | } 9 | 10 | // collection of properties listed here: 11 | // https://developer.mozilla.org/en-US/docs/Web/CSS/background 12 | pub struct Background { 13 | pub mut: 14 | color ?ColorValue 15 | image ?Image 16 | } 17 | 18 | // collection of properties listed here: 19 | // https://developer.mozilla.org/en-US/docs/Web/CSS/border 20 | pub struct Border { 21 | pub mut: 22 | collapse Keyword = 'separate' 23 | colors BorderColors 24 | styles BorderStyles 25 | widths FourDimensions 26 | radius BorderRadius 27 | } 28 | 29 | // used for shorthand properties of border-bottom, border-left etc. 30 | pub struct SingleBorder { 31 | pub mut: 32 | color ?ColorValue 33 | style BorderLineStyle = datatypes.LineStyle.@none 34 | width DimensionValue = css.zero_px 35 | } 36 | 37 | // border-color 38 | pub struct BorderColors { 39 | pub mut: 40 | top ?ColorValue 41 | right ?ColorValue 42 | bottom ?ColorValue 43 | left ?ColorValue 44 | } 45 | 46 | // border-style 47 | pub struct BorderStyles { 48 | pub mut: 49 | top BorderLineStyle = datatypes.LineStyle.@none 50 | right BorderLineStyle = datatypes.LineStyle.@none 51 | bottom BorderLineStyle = datatypes.LineStyle.@none 52 | left BorderLineStyle = datatypes.LineStyle.@none 53 | } 54 | 55 | // border-radius 56 | pub struct BorderRadius { 57 | pub mut: 58 | top_left SingleBorderRadius = []DimensionValue{len: 2, init: css.zero_px} 59 | top_right SingleBorderRadius = []DimensionValue{len: 2, init: css.zero_px} 60 | bottom_right SingleBorderRadius = []DimensionValue{len: 2, init: css.zero_px} 61 | bottom_left SingleBorderRadius = []DimensionValue{len: 2, init: css.zero_px} 62 | } 63 | 64 | // border-top-left-radius 65 | pub type SingleBorderRadius = []DimensionValue 66 | 67 | // For properties like `margin` and `padding` that can have 4 dimensions values 68 | // https://developer.mozilla.org/en-US/docs/Web/CSS/margin 69 | pub struct FourDimensions { 70 | pub mut: 71 | top DimensionValue = css.zero_px 72 | right DimensionValue = css.zero_px 73 | bottom DimensionValue = css.zero_px 74 | left DimensionValue = css.zero_px 75 | } 76 | 77 | pub struct Font { 78 | pub mut: 79 | family FontFamily 80 | size DimensionValue = datatypes.Length{ 81 | amount: 16 82 | unit: .px 83 | } 84 | stretch FontStretch 85 | style Keyword = Keyword('normal') 86 | weight FontWeight = 400 87 | } 88 | 89 | pub struct Text { 90 | pub mut: 91 | align ?string 92 | align_last ?string 93 | combine_upright ?TextCombineUpright 94 | // decoration 95 | // emphasis 96 | indent ?DimensionValue 97 | justify ?string 98 | orientation ?string 99 | overflow ?TextOverflow 100 | rendering ?string 101 | shadow []Shadow 102 | transform ?string 103 | wrap ?string 104 | } 105 | 106 | pub struct Shadow { 107 | pub mut: 108 | offset_x DimensionValue 109 | offset_y DimensionValue 110 | blur_radius DimensionValue = css.zero_px 111 | spread_radius DimensionValue = css.zero_px 112 | inset bool 113 | color ColorValue 114 | } 115 | 116 | pub struct Overflow { 117 | pub mut: 118 | overflow_x Keyword = 'visible' 119 | overflow_y Keyword = 'visible' 120 | } 121 | 122 | pub struct FlexBox { 123 | pub mut: 124 | basis DimensionValue = Keyword('auto') 125 | direction FlexDirection = datatypes.FlexDirectionKind.row 126 | // negative numbers are invalid 127 | grow FlexSize = 0.0 128 | // negative numbers are invalid 129 | shrink FlexSize = 1.0 130 | wrap FlexWrap = datatypes.FlexWrapKind.nowrap 131 | } 132 | 133 | pub struct Gap { 134 | pub mut: 135 | row DimensionValue = css.zero_px 136 | column DimensionValue = css.zero_px 137 | } 138 | -------------------------------------------------------------------------------- /css/tests/compiler_errors_test.v: -------------------------------------------------------------------------------- 1 | import benchmark 2 | import os 3 | import rand 4 | import runtime 5 | import term 6 | import time 7 | import v.util.diff 8 | 9 | const test_program_name = 'test_program.v' 10 | 11 | const turn_off_vcolors = os.setenv('VCOLORS', 'never', true) 12 | 13 | const github_job = os.getenv('GITHUB_JOB') 14 | 15 | struct TaskDescription { 16 | dir string 17 | result_extension string 18 | path string 19 | program string 20 | mut: 21 | is_error bool 22 | is_skipped bool 23 | expected string 24 | expected_out_path string 25 | found___ string 26 | took time.Duration 27 | cli_cmd string 28 | } 29 | 30 | struct Tasks { 31 | parallel_jobs int // 0 is using VJOBS, anything else is an override 32 | label string 33 | mut: 34 | all []TaskDescription 35 | } 36 | 37 | fn test_all() { 38 | os.chdir(@VMODROOT)! 39 | parser_dir := 'css/parser/tests' 40 | checker_dir := 'css/checker/tests' 41 | 42 | mut tasks := Tasks{ 43 | label: 'all tests' 44 | } 45 | 46 | parser_tests := get_tests_in_dir(parser_dir) 47 | checker_tests := get_tests_in_dir(checker_dir) 48 | tasks.add(parser_dir, '.out', parser_tests, 'css/parser/tests/test_program.v') 49 | tasks.add(checker_dir, '.out', checker_tests, 'css/checker/tests/test_program.v') 50 | 51 | tasks.run() 52 | } 53 | 54 | fn (mut tasks Tasks) add(dir string, result_extension string, tests []string, program string) { 55 | program_id := rand.ulid() 56 | exec_path := os.join_path(os.vtmp_dir(), 'css_test_${program_id}') 57 | eprintln('EXECUTING: ${exec_path}') 58 | mut res := os.execute('${os.quoted_path(@VEXE)} ${program} -o ${exec_path}') 59 | dump(res) 60 | 61 | for path in tests { 62 | tasks.all << TaskDescription{ 63 | program: exec_path 64 | dir: dir 65 | result_extension: result_extension 66 | path: os.join_path_single(dir, path) 67 | } 68 | } 69 | } 70 | 71 | fn bstep_message(mut bench benchmark.Benchmark, label string, msg string, sduration time.Duration) string { 72 | return bench.step_message_with_label_and_duration(label, msg, sduration) 73 | } 74 | 75 | // process an array of tasks in parallel, using no more than vjobs worker threads 76 | fn (mut tasks Tasks) run() { 77 | if tasks.all.len == 0 { 78 | return 79 | } 80 | vjobs := if tasks.parallel_jobs > 0 { tasks.parallel_jobs } else { runtime.nr_jobs() } 81 | mut bench := benchmark.new_benchmark() 82 | bench.set_total_expected_steps(tasks.all.len) 83 | mut work := chan TaskDescription{cap: tasks.all.len} 84 | mut results := chan TaskDescription{cap: tasks.all.len} 85 | 86 | for i in 0 .. tasks.all.len { 87 | work <- tasks.all[i] 88 | } 89 | work.close() 90 | if github_job == '' { 91 | println('') 92 | } 93 | for _ in 0 .. vjobs { 94 | spawn work_processor(work, results) 95 | } 96 | 97 | mut line_can_be_erased := true 98 | mut total_errors := 0 99 | for _ in 0 .. tasks.all.len { 100 | mut task := TaskDescription{} 101 | task = <-results 102 | bench.step() 103 | if task.is_error { 104 | total_errors++ 105 | bench.fail() 106 | eprintln(bstep_message(mut bench, benchmark.b_fail, task.path, task.took)) 107 | println('============') 108 | println('failed cmd: ${task.cli_cmd}') 109 | println('expected_out_path: ${task.expected_out_path}') 110 | println('============') 111 | println('expected:') 112 | println(task.expected) 113 | println('============') 114 | println('found:') 115 | println(task.found___) 116 | println('============\n') 117 | diff_content(task.expected, task.found___) 118 | line_can_be_erased = false 119 | } else { 120 | bench.ok() 121 | assert true 122 | if github_job == '' { 123 | // local mode: 124 | if line_can_be_erased { 125 | term.clear_previous_line() 126 | } 127 | println(bstep_message(mut bench, benchmark.b_ok, task.path, task.took)) 128 | } 129 | line_can_be_erased = true 130 | } 131 | } 132 | bench.stop() 133 | eprintln(term.h_divider('-')) 134 | eprintln(bench.total_message(tasks.label)) 135 | if total_errors != 0 { 136 | exit(1) 137 | } 138 | } 139 | 140 | // a single worker thread spends its time getting work from the `work` channel, 141 | // processing the task, and then putting the task in the `results` channel 142 | fn work_processor(work chan TaskDescription, results chan TaskDescription) { 143 | for { 144 | mut task := <-work or { break } 145 | sw := time.new_stopwatch() 146 | task.execute() 147 | task.took = sw.elapsed() 148 | results <- task 149 | } 150 | } 151 | 152 | // actual processing; Note: no output is done here at all 153 | fn (mut task TaskDescription) execute() { 154 | if task.is_skipped { 155 | return 156 | } 157 | css_file := task.path 158 | strict_arg := if css_file.ends_with('_strict.css') { 159 | ' --strict' 160 | } else { 161 | '' 162 | } 163 | cli_cmd := '${task.program} ${css_file}${strict_arg}' 164 | res := os.execute(cli_cmd) 165 | expected_out_path := css_file.replace('.css', '') + task.result_extension 166 | task.expected_out_path = expected_out_path 167 | task.cli_cmd = cli_cmd 168 | 169 | mut expected := os.read_file(expected_out_path) or { 170 | eprintln('FAILED!!! ${css_file}') 171 | panic(err) 172 | } 173 | task.expected = term.strip_ansi(clean_line_endings(expected)) 174 | task.found___ = term.strip_ansi(clean_line_endings(res.output)) 175 | 176 | if task.expected != task.found___ { 177 | task.is_error = true 178 | } 179 | } 180 | 181 | fn clean_line_endings(s string) string { 182 | mut res := s.trim_space() 183 | res = res.replace(' \n', '\n') 184 | res = res.replace(' \r\n', '\n') 185 | res = res.replace('\r\n', '\n') 186 | res = res.trim('\n') 187 | return res 188 | } 189 | 190 | fn diff_content(expected string, found string) { 191 | diff_cmd := diff.find_working_diff_command() or { return } 192 | println(term.bold(term.yellow('diff: '))) 193 | println(diff.color_compare_strings(diff_cmd, rand.ulid(), expected, found)) 194 | println('============\n') 195 | eprintln('got ${found}') 196 | } 197 | 198 | fn get_tests_in_dir(dir string) []string { 199 | eprintln('tests ${dir} from ${os.getwd()}') 200 | files := os.ls(dir) or { panic(err) } 201 | mut tests := files.clone() 202 | tests = files.filter(it.ends_with('.css')) 203 | tests.sort() 204 | return tests 205 | } 206 | -------------------------------------------------------------------------------- /css/tests/functions/gradient_test.v: -------------------------------------------------------------------------------- 1 | import css 2 | import css.datatypes 3 | import css.pref 4 | import css.util as css_util 5 | 6 | const preferences = pref.Preferences{} 7 | 8 | fn test_gradient_simple() { 9 | rules := css_util.parse_stylesheet_from_text('.t { background-image: linear-gradient(red, green); }', 10 | preferences)! 11 | styles := rules.get_styles() 12 | 13 | assert styles == { 14 | 'background': css.Background{ 15 | image: css.Gradient{ 16 | kind: .linear 17 | gradient_values: [css.GradientValue{ 18 | color: 'red' 19 | }, css.GradientValue{ 20 | color: 'green' 21 | }] 22 | } 23 | } 24 | } 25 | } 26 | 27 | fn test_gradient_directions() { 28 | rules := css_util.parse_stylesheet_from_text('.t { background-image: linear-gradient(to right bottom, red, green); }', 29 | preferences)! 30 | styles := rules.get_styles() 31 | 32 | assert styles == { 33 | 'background': css.Background{ 34 | image: css.Gradient{ 35 | kind: .linear 36 | directions: .right | .bottom 37 | gradient_values: [css.GradientValue{ 38 | color: 'red' 39 | }, css.GradientValue{ 40 | color: 'green' 41 | }] 42 | } 43 | } 44 | } 45 | } 46 | 47 | fn test_gradient_dimensions() { 48 | rules := css_util.parse_stylesheet_from_text('.t { background-image: radial-gradient(red 10px, green 60%); }', 49 | preferences)! 50 | styles := rules.get_styles() 51 | 52 | assert styles == { 53 | 'background': css.Background{ 54 | image: css.Gradient{ 55 | kind: .radial 56 | gradient_values: [ 57 | css.GradientValue{ 58 | color: 'red' 59 | size: datatypes.Length{ 60 | amount: 10 61 | unit: .px 62 | } 63 | }, 64 | css.GradientValue{ 65 | color: 'green' 66 | size: datatypes.Percentage(0.6) 67 | }, 68 | ] 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /css/tests/functions/rgb_test.v: -------------------------------------------------------------------------------- 1 | import css 2 | import css.datatypes 3 | import css.pref 4 | import css.util as css_util 5 | 6 | const preferences = pref.Preferences{} 7 | 8 | fn test_rgb() { 9 | rules := css_util.parse_stylesheet_from_text('.t { color: rgb(10 20 30); }', preferences)! 10 | styles := rules.get_styles() 11 | 12 | assert styles == { 13 | 'color': css.Value(css.ColorValue(datatypes.Color{ 14 | r: 10 15 | g: 20 16 | b: 30 17 | a: 255 18 | })) 19 | } 20 | } 21 | 22 | fn test_rgba() { 23 | rules := css_util.parse_stylesheet_from_text('.t { color: rgb(10 20 30 / 20%); }', 24 | preferences)! 25 | styles := rules.get_styles() 26 | 27 | assert styles == { 28 | 'color': css.Value(css.ColorValue(datatypes.Color{ 29 | r: 10 30 | g: 20 31 | b: 30 32 | a: u8(255 * 0.2) 33 | })) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /css/tests/functions/url_test.v: -------------------------------------------------------------------------------- 1 | import css 2 | import css.datatypes 3 | import css.pref 4 | import css.util as css_util 5 | 6 | const preferences = pref.Preferences{} 7 | 8 | fn test_url_element() { 9 | rules := css_util.parse_stylesheet_from_text('.t { background-image: url(#element); }', 10 | preferences)! 11 | styles := rules.get_styles() 12 | 13 | assert styles == { 14 | 'background': css.Background{ 15 | image: css.Url{ 16 | kind: .element 17 | value: 'element' 18 | } 19 | } 20 | } 21 | } 22 | 23 | fn test_url_data() { 24 | rules := css_util.parse_stylesheet_from_text('.t { background-image: url(data:image/png;base64,); }', 25 | preferences)! 26 | styles := rules.get_styles() 27 | 28 | assert styles == { 29 | 'background': css.Background{ 30 | image: css.Url{ 31 | kind: .data 32 | value: 'data:image/png;base64,' 33 | } 34 | } 35 | } 36 | } 37 | 38 | fn test_url_link_http() { 39 | rules := css_util.parse_stylesheet_from_text('.t { background-image: url( http://a.com/image.png); }', 40 | preferences)! 41 | styles := rules.get_styles() 42 | 43 | assert styles == { 44 | 'background': css.Background{ 45 | image: css.Url{ 46 | kind: .link 47 | value: 'http://a.com/image.png' 48 | } 49 | } 50 | } 51 | } 52 | 53 | fn test_url_file() { 54 | rules := css_util.parse_stylesheet_from_text('.t { background-image: url(myFont.woff); }', 55 | preferences)! 56 | styles := rules.get_styles() 57 | 58 | assert styles == { 59 | 'background': css.Background{ 60 | image: css.Url{ 61 | kind: .file 62 | value: 'myFont.woff' 63 | } 64 | } 65 | } 66 | } 67 | 68 | fn test_fallback() { 69 | rules := css_util.parse_stylesheet_from_text('.t { background-image: url("image.png"), pointer; }', 70 | preferences)! 71 | styles := rules.get_styles() 72 | 73 | assert styles == { 74 | 'background': css.Background{ 75 | image: css.Url{ 76 | kind: .file 77 | value: 'image.png' 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /css/tests/properties/alpha_test.v: -------------------------------------------------------------------------------- 1 | import css 2 | import css.pref 3 | import css.util as css_util 4 | 5 | const preferences = pref.Preferences{} 6 | 7 | fn test_alpha_value_percentage() { 8 | rules := css_util.parse_stylesheet_from_text('.t { opacity: 90%; }', preferences)! 9 | styles := rules.get_styles() 10 | 11 | assert styles == { 12 | 'opacity': css.Value(css.AlphaValue(0.9)) 13 | } 14 | } 15 | 16 | fn test_alpha_value_number() { 17 | rules := css_util.parse_stylesheet_from_text('.t { opacity: 0.4; }', preferences)! 18 | styles := rules.get_styles() 19 | 20 | assert styles == { 21 | 'opacity': css.Value(css.AlphaValue(0.4)) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /css/tests/properties/background_test.v: -------------------------------------------------------------------------------- 1 | import css 2 | import css.pref 3 | import css.util as css_util 4 | 5 | const preferences = pref.Preferences{} 6 | 7 | fn test_background_can_group() { 8 | rules := css_util.parse_stylesheet_from_text('.t { 9 | background-color: red; 10 | background-image: linear-gradient(red, green); 11 | }')! 12 | styles := rules.get_styles() 13 | 14 | assert styles == { 15 | 'background': css.Background{ 16 | color: 'red' 17 | image: css.Gradient{ 18 | kind: .linear 19 | gradient_values: [css.GradientValue{ 20 | color: 'red' 21 | }, css.GradientValue{ 22 | color: 'green' 23 | }] 24 | } 25 | } 26 | } 27 | } 28 | 29 | fn test_background_can_merge() { 30 | rules := css_util.parse_stylesheet_from_text('.t { 31 | background-color: red; 32 | } 33 | .u { 34 | background-image: linear-gradient(red, green); 35 | }')! 36 | styles := rules.get_styles() 37 | 38 | assert styles == { 39 | 'background': css.Background{ 40 | color: 'red' 41 | image: css.Gradient{ 42 | kind: .linear 43 | gradient_values: [css.GradientValue{ 44 | color: 'red' 45 | }, css.GradientValue{ 46 | color: 'green' 47 | }] 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /css/tests/properties/browser_prefix_test.v: -------------------------------------------------------------------------------- 1 | import css 2 | import css.datatypes 3 | import css.pref 4 | import css.util as css_util 5 | 6 | const preferences = pref.Preferences{} 7 | 8 | // -webkit- and -moz- should end up in the styles map, 9 | // but be parsed as if the browser prefix isn't present. 10 | 11 | fn test_webkit() { 12 | rules := css_util.parse_stylesheet_from_text('.t { -webkit-color: red; }', preferences)! 13 | styles := rules.get_styles() 14 | 15 | assert styles == { 16 | '-webkit-color': css.Value(css.ColorValue('red')) 17 | } 18 | } 19 | 20 | fn test_moz() { 21 | rules := css_util.parse_stylesheet_from_text('.t { -moz-color: red; }', preferences)! 22 | styles := rules.get_styles() 23 | 24 | assert styles == { 25 | '-moz-color': css.Value(css.ColorValue('red')) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /css/tests/properties/color_test.v: -------------------------------------------------------------------------------- 1 | import css 2 | import css.datatypes 3 | import css.pref 4 | import css.util as css_util 5 | 6 | const preferences = pref.Preferences{} 7 | 8 | fn test_color_name() { 9 | rules := css_util.parse_stylesheet_from_text('.t { color: red; }', preferences)! 10 | styles := rules.get_styles() 11 | 12 | assert styles == { 13 | 'color': css.Value(css.ColorValue('red')) 14 | } 15 | } 16 | 17 | fn test_color_hex() { 18 | rules := css_util.parse_stylesheet_from_text('.t { color: #ffffff; }', preferences)! 19 | styles := rules.get_styles() 20 | 21 | assert styles == { 22 | 'color': css.Value(css.ColorValue(datatypes.Color{ 23 | r: 255 24 | g: 255 25 | b: 255 26 | a: 255 27 | })) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /css/tests/properties/flex_test.v: -------------------------------------------------------------------------------- 1 | import css 2 | import css.datatypes 3 | import css.pref 4 | import css.util as css_util 5 | 6 | const preferences = pref.Preferences{} 7 | 8 | fn test_flex_direction() { 9 | rules := css_util.parse_stylesheet_from_text('.t { flex-direction: column; }', preferences)! 10 | styles := rules.get_styles() 11 | 12 | assert styles == { 13 | 'flex': css.FlexBox{ 14 | direction: datatypes.FlexDirectionKind.column 15 | } 16 | } 17 | } 18 | 19 | fn test_flex_size() { 20 | rules := css_util.parse_stylesheet_from_text('.t { flex-grow: 2.5; flex-shrink: inherit; }', 21 | preferences)! 22 | styles := rules.get_styles() 23 | 24 | assert styles == { 25 | 'flex': css.FlexBox{ 26 | grow: 2.5 27 | shrink: css.Keyword('inherit') 28 | } 29 | } 30 | } 31 | 32 | fn test_flex_wrap() { 33 | rules := css_util.parse_stylesheet_from_text('.t { flex-wrap: wrap-reverse; }', preferences)! 34 | styles := rules.get_styles() 35 | 36 | assert styles == { 37 | 'flex': css.FlexBox{ 38 | wrap: datatypes.FlexWrapKind.wrap_reverse 39 | } 40 | } 41 | } 42 | 43 | fn test_flex_one() { 44 | mut rules := css_util.parse_stylesheet_from_text('.t { flex: 2; }', preferences)! 45 | mut styles := rules.get_styles() 46 | 47 | assert styles == { 48 | 'flex': css.FlexBox{ 49 | grow: 2.0 50 | basis: 0.0 51 | } 52 | } 53 | 54 | rules = css_util.parse_stylesheet_from_text('.t { flex: auto; }', preferences)! 55 | styles = rules.get_styles() 56 | 57 | assert styles == { 58 | 'flex': css.FlexBox{ 59 | grow: 1.0 60 | shrink: 1.0 61 | basis: css.Keyword('auto') 62 | } 63 | } 64 | } 65 | 66 | fn test_flex_two() { 67 | mut rules := css_util.parse_stylesheet_from_text('.t { flex: 1 30px; }', preferences)! 68 | mut styles := rules.get_styles() 69 | 70 | assert styles == { 71 | 'flex': css.FlexBox{ 72 | grow: 1.0 73 | shrink: 1.0 74 | basis: datatypes.Length{ 75 | amount: 30 76 | unit: .px 77 | } 78 | } 79 | } 80 | 81 | rules = css_util.parse_stylesheet_from_text('.t { flex: 2 2; }', preferences)! 82 | styles = rules.get_styles() 83 | 84 | assert styles == { 85 | 'flex': css.FlexBox{ 86 | grow: 2.0 87 | shrink: 2.0 88 | basis: 0.0 89 | } 90 | } 91 | } 92 | 93 | fn test_flex_three() { 94 | mut rules := css_util.parse_stylesheet_from_text('.t { flex: 2 2 10%; }', preferences)! 95 | mut styles := rules.get_styles() 96 | 97 | assert styles == { 98 | 'flex': css.FlexBox{ 99 | grow: 2.0 100 | shrink: 2.0 101 | basis: datatypes.Percentage(0.1) 102 | } 103 | } 104 | } 105 | 106 | fn test_flex_flow() { 107 | mut rules := css_util.parse_stylesheet_from_text('.t { flex-flow: column wrap; }', 108 | preferences)! 109 | mut styles := rules.get_styles() 110 | 111 | assert styles == { 112 | 'flex': css.FlexBox{ 113 | direction: datatypes.FlexDirectionKind.column 114 | wrap: datatypes.FlexWrapKind.wrap 115 | } 116 | } 117 | } 118 | 119 | fn test_flex_flow_keyword() { 120 | mut rules := css_util.parse_stylesheet_from_text('.t { flex-flow: inherit; }', preferences)! 121 | mut styles := rules.get_styles() 122 | 123 | assert styles == { 124 | 'flex': css.FlexBox{ 125 | direction: css.Keyword('inherit') 126 | wrap: css.Keyword('inherit') 127 | } 128 | } 129 | } 130 | 131 | fn test_flex_merged() { 132 | mut rules := css_util.parse_stylesheet_from_text('.t { 133 | flex: 2 2 10%; 134 | flex-flow: column wrap; 135 | flex-wrap: nowrap; 136 | flex-shrink: 1; 137 | }', 138 | preferences)! 139 | mut styles := rules.get_styles() 140 | 141 | assert styles == { 142 | 'flex': css.FlexBox{ 143 | grow: 2.0 144 | shrink: 1.0 145 | basis: datatypes.Percentage(0.1) 146 | wrap: datatypes.FlexWrapKind.nowrap 147 | direction: datatypes.FlexDirectionKind.column 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /css/tests/properties/font_test.v: -------------------------------------------------------------------------------- 1 | import css 2 | import css.datatypes 3 | import css.pref 4 | import css.util as css_util 5 | 6 | const preferences = pref.Preferences{} 7 | 8 | fn test_font_family() { 9 | rules := css_util.parse_stylesheet_from_text('.t { font-family: "Courier New", Courier, monospace; }', 10 | preferences)! 11 | styles := rules.get_styles() 12 | 13 | assert styles == { 14 | 'font': css.Font{ 15 | family: ['Courier New', 'Courier', 'monospace'] 16 | } 17 | } 18 | } 19 | 20 | fn test_font_stretch() { 21 | mut rules := css_util.parse_stylesheet_from_text('.t { font-stretch: semi-condensed; }', 22 | preferences)! 23 | mut styles := rules.get_styles() 24 | 25 | assert styles == { 26 | 'font': css.Font{ 27 | stretch: datatypes.FontStretchKind.semi_condensed 28 | } 29 | } 30 | 31 | rules = css_util.parse_stylesheet_from_text('.t { font-stretch: 25%; }', preferences)! 32 | styles = rules.get_styles() 33 | 34 | assert styles == { 35 | 'font': css.Font{ 36 | stretch: datatypes.Percentage(0.25) 37 | } 38 | } 39 | 40 | rules = css_util.parse_stylesheet_from_text('.t { font-stretch: inherit; }', preferences)! 41 | styles = rules.get_styles() 42 | 43 | assert styles == { 44 | 'font': css.Font{ 45 | stretch: css.Keyword('inherit') 46 | } 47 | } 48 | } 49 | 50 | fn test_font_weight() { 51 | mut rules := css_util.parse_stylesheet_from_text('.t { font-weight: 392; }', preferences)! 52 | mut styles := rules.get_styles() 53 | 54 | assert styles == { 55 | 'font': css.Font{ 56 | weight: 392 57 | } 58 | } 59 | 60 | rules = css_util.parse_stylesheet_from_text('.t { font-weight: 100.9; }', preferences)! 61 | styles = rules.get_styles() 62 | 63 | assert styles == { 64 | 'font': css.Font{ 65 | weight: 100 66 | } 67 | } 68 | 69 | rules = css_util.parse_stylesheet_from_text('.t { font-weight: normal; }', preferences)! 70 | styles = rules.get_styles() 71 | 72 | assert styles == { 73 | 'font': css.Font{ 74 | weight: css.Keyword('normal') 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /css/tests/properties/margin_padding_test.v: -------------------------------------------------------------------------------- 1 | import css 2 | import css.datatypes 3 | import css.pref 4 | import css.util as css_util 5 | 6 | const preferences = pref.Preferences{} 7 | 8 | fn test_single_dimensions() { 9 | rules := css_util.parse_stylesheet_from_text('.t { padding-left: 100vw; margin-right: 20% }', 10 | preferences)! 11 | styles := rules.get_styles() 12 | 13 | assert styles == { 14 | 'padding': css.FourDimensions{ 15 | left: datatypes.Length{ 16 | amount: 100 17 | unit: .vw 18 | } 19 | } 20 | 'margin': css.FourDimensions{ 21 | right: datatypes.Percentage(0.2) 22 | } 23 | } 24 | } 25 | 26 | fn test_grouped_4() { 27 | rules := css_util.parse_stylesheet_from_text('.t { padding: 10px 20px 30px 40px; }', 28 | preferences)! 29 | styles := rules.get_styles() 30 | 31 | assert styles == { 32 | 'padding': css.Value(css.FourDimensions{ 33 | top: css.DimensionValue(datatypes.Length{ 34 | amount: 10 35 | unit: .px 36 | }) 37 | right: css.DimensionValue(datatypes.Length{ 38 | amount: 20 39 | unit: .px 40 | }) 41 | bottom: css.DimensionValue(datatypes.Length{ 42 | amount: 30 43 | unit: .px 44 | }) 45 | left: css.DimensionValue(datatypes.Length{ 46 | amount: 40 47 | unit: .px 48 | }) 49 | }) 50 | } 51 | } 52 | 53 | fn test_grouped_3() { 54 | rules := css_util.parse_stylesheet_from_text('.t { padding: 10px 20px 30px; }', preferences)! 55 | styles := rules.get_styles() 56 | 57 | assert styles == { 58 | 'padding': css.Value(css.FourDimensions{ 59 | top: css.DimensionValue(datatypes.Length{ 60 | amount: 10 61 | unit: .px 62 | }) 63 | right: css.DimensionValue(datatypes.Length{ 64 | amount: 20 65 | unit: .px 66 | }) 67 | bottom: css.DimensionValue(datatypes.Length{ 68 | amount: 30 69 | unit: .px 70 | }) 71 | left: css.DimensionValue(datatypes.Length{ 72 | amount: 20 73 | unit: .px 74 | }) 75 | }) 76 | } 77 | } 78 | 79 | fn test_grouped_2() { 80 | rules := css_util.parse_stylesheet_from_text('.t { padding: 10px 20px; }', preferences)! 81 | styles := rules.get_styles() 82 | 83 | assert styles == { 84 | 'padding': css.Value(css.FourDimensions{ 85 | top: css.DimensionValue(datatypes.Length{ 86 | amount: 10 87 | unit: .px 88 | }) 89 | right: css.DimensionValue(datatypes.Length{ 90 | amount: 20 91 | unit: .px 92 | }) 93 | bottom: css.DimensionValue(datatypes.Length{ 94 | amount: 10 95 | unit: .px 96 | }) 97 | left: css.DimensionValue(datatypes.Length{ 98 | amount: 20 99 | unit: .px 100 | }) 101 | }) 102 | } 103 | } 104 | 105 | fn test_grouped_1() { 106 | rules := css_util.parse_stylesheet_from_text('.t { padding: 10px; }', preferences)! 107 | styles := rules.get_styles() 108 | 109 | assert styles == { 110 | 'padding': css.Value(css.FourDimensions{ 111 | top: css.DimensionValue(datatypes.Length{ 112 | amount: 10 113 | unit: .px 114 | }) 115 | right: css.DimensionValue(datatypes.Length{ 116 | amount: 10 117 | unit: .px 118 | }) 119 | bottom: css.DimensionValue(datatypes.Length{ 120 | amount: 10 121 | unit: .px 122 | }) 123 | left: css.DimensionValue(datatypes.Length{ 124 | amount: 10 125 | unit: .px 126 | }) 127 | }) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /css/tests/properties/overflow_test.v: -------------------------------------------------------------------------------- 1 | import css 2 | import css.datatypes 3 | import css.pref 4 | import css.util as css_util 5 | 6 | const preferences = pref.Preferences{} 7 | 8 | fn test_single_keyword() { 9 | rules := css_util.parse_stylesheet_from_text('.t { overflow: hidden; }', preferences)! 10 | styles := rules.get_styles() 11 | 12 | assert styles == { 13 | 'overflow': css.Overflow{ 14 | overflow_x: 'hidden' 15 | overflow_y: 'hidden' 16 | } 17 | } 18 | } 19 | 20 | fn test_individual_keyword() { 21 | rules := css_util.parse_stylesheet_from_text('.t { overflow-x: hidden; }', preferences)! 22 | styles := rules.get_styles() 23 | 24 | assert styles == { 25 | 'overflow': css.Overflow{ 26 | overflow_x: 'hidden' 27 | } 28 | } 29 | } 30 | 31 | fn test_merged() { 32 | rules := css_util.parse_stylesheet_from_text('.t { overflow-x: hidden; overflow-y: visible; }', 33 | preferences)! 34 | styles := rules.get_styles() 35 | 36 | assert styles == { 37 | 'overflow': css.Overflow{ 38 | overflow_x: 'hidden' 39 | overflow_y: 'visible' 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /css/tests/properties/shadow_test.v: -------------------------------------------------------------------------------- 1 | import css 2 | import css.datatypes 3 | import css.pref 4 | import css.util as css_util 5 | 6 | const preferences = pref.Preferences{} 7 | 8 | fn test_shadow_keyword() { 9 | rules := css_util.parse_stylesheet_from_text('.t { box-shadow: none; }', preferences)! 10 | styles := rules.get_styles() 11 | 12 | assert styles == { 13 | 'box-shadow': css.Keyword('none') 14 | } 15 | } 16 | 17 | fn test_two_lengths() { 18 | rules := css_util.parse_stylesheet_from_text('.t { box-shadow: 2px 4px red; }', preferences)! 19 | styles := rules.get_styles() 20 | 21 | assert styles == { 22 | 'box-shadow': css.Shadow{ 23 | offset_x: datatypes.Length{ 24 | amount: 2 25 | unit: .px 26 | } 27 | offset_y: datatypes.Length{ 28 | amount: 4 29 | unit: .px 30 | } 31 | color: 'red' 32 | } 33 | } 34 | } 35 | 36 | fn test_three_lengths() { 37 | rules := css_util.parse_stylesheet_from_text('.t { box-shadow: 2px 4px .6px red; }', 38 | preferences)! 39 | styles := rules.get_styles() 40 | 41 | assert styles == { 42 | 'box-shadow': css.Shadow{ 43 | offset_x: datatypes.Length{ 44 | amount: 2 45 | unit: .px 46 | } 47 | offset_y: datatypes.Length{ 48 | amount: 4 49 | unit: .px 50 | } 51 | blur_radius: datatypes.Length{ 52 | amount: 0.6 53 | unit: .px 54 | } 55 | color: 'red' 56 | } 57 | } 58 | } 59 | 60 | fn test_four_lengths() { 61 | rules := css_util.parse_stylesheet_from_text('.t { box-shadow: 2px 4px .6px 1em red; }', 62 | preferences)! 63 | styles := rules.get_styles() 64 | 65 | assert styles == { 66 | 'box-shadow': css.Shadow{ 67 | offset_x: datatypes.Length{ 68 | amount: 2 69 | unit: .px 70 | } 71 | offset_y: datatypes.Length{ 72 | amount: 4 73 | unit: .px 74 | } 75 | blur_radius: datatypes.Length{ 76 | amount: 0.6 77 | unit: .px 78 | } 79 | spread_radius: datatypes.Length{ 80 | amount: 1 81 | unit: .em 82 | } 83 | color: 'red' 84 | } 85 | } 86 | } 87 | 88 | fn test_inset() { 89 | rules := css_util.parse_stylesheet_from_text('.t { box-shadow: inset 2px 4px red; }', 90 | preferences)! 91 | styles := rules.get_styles() 92 | 93 | assert styles == { 94 | 'box-shadow': css.Shadow{ 95 | inset: true 96 | offset_x: datatypes.Length{ 97 | amount: 2 98 | unit: .px 99 | } 100 | offset_y: datatypes.Length{ 101 | amount: 4 102 | unit: .px 103 | } 104 | color: 'red' 105 | } 106 | } 107 | } 108 | 109 | fn test_all() { 110 | rules := css_util.parse_stylesheet_from_text('.t { box-shadow: inset 2px 4px .6px 1em rgba( 0 0 0 / 20%); }', 111 | preferences)! 112 | styles := rules.get_styles() 113 | 114 | assert styles == { 115 | 'box-shadow': css.Shadow{ 116 | inset: true 117 | offset_x: datatypes.Length{ 118 | amount: 2 119 | unit: .px 120 | } 121 | offset_y: datatypes.Length{ 122 | amount: 4 123 | unit: .px 124 | } 125 | blur_radius: datatypes.Length{ 126 | amount: 0.6 127 | unit: .px 128 | } 129 | spread_radius: datatypes.Length{ 130 | amount: 1 131 | unit: .em 132 | } 133 | color: datatypes.Color{ 134 | a: u8(255 * 0.2) 135 | } 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /css/tests/properties/single_dimension_test.v: -------------------------------------------------------------------------------- 1 | import css 2 | import css.datatypes 3 | import css.pref 4 | import css.util as css_util 5 | 6 | const preferences = pref.Preferences{} 7 | 8 | fn test_length() { 9 | rules := css_util.parse_stylesheet_from_text('.t { width: 100px; }', preferences)! 10 | styles := rules.get_styles() 11 | 12 | assert styles == { 13 | 'width': css.Value(css.DimensionValue(datatypes.Length{ 14 | amount: 100 15 | unit: .px 16 | })) 17 | } 18 | } 19 | 20 | fn test_percentage() { 21 | rules := css_util.parse_stylesheet_from_text('.t { width: 110%; }', preferences)! 22 | styles := rules.get_styles() 23 | 24 | assert styles == { 25 | 'width': css.Value(css.DimensionValue(datatypes.Percentage(1.1))) 26 | } 27 | } 28 | 29 | fn test_zero() { 30 | rules := css_util.parse_stylesheet_from_text('.t { width: 0; top: 0; }', preferences)! 31 | styles := rules.get_styles() 32 | 33 | assert styles == { 34 | 'width': css.Value(css.DimensionValue(0.0)) 35 | 'top': css.Value(css.DimensionValue(0.0)) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /css/tests/selector_test.v: -------------------------------------------------------------------------------- 1 | import css 2 | 3 | fn test_selector() { 4 | mut s1 := []css.Selector{} 5 | mut s2 := []css.Selector{} 6 | 7 | s1 = [css.Class('test')] 8 | s2 = [css.Id('what'), css.Class('test')] 9 | 10 | // s1 is a subset of s2 11 | assert s1.matches(s2) == true 12 | // s2 is not a subset of s1 13 | assert s2.matches(s1) == false 14 | } 15 | 16 | fn test_different_types_same_values() { 17 | mut s1 := []css.Selector{} 18 | mut s2 := []css.Selector{} 19 | 20 | s1 = [css.Class('test')] 21 | s2 = [css.Id('test')] 22 | 23 | assert s1.matches(s2) == false 24 | assert s2.matches(s1) == false 25 | } 26 | 27 | fn test_skipping_combinators() { 28 | mut s1 := []css.Selector{} 29 | mut s2 := []css.Selector{} 30 | 31 | s1 = [css.Class('test'), css.Combinator('>'), css.Id('what')] 32 | s2 = [css.Class('test')] 33 | 34 | assert s1.matches(s2) == false 35 | } 36 | 37 | fn test_attribute() { 38 | assert css.Attribute{ 39 | name: 'href' 40 | } != css.Attribute{ 41 | name: 'id' 42 | } 43 | 44 | assert css.Attribute{ 45 | name: 'href' 46 | matcher: .exact 47 | value: '#test' 48 | } != css.Attribute{ 49 | name: 'href' 50 | } 51 | 52 | assert css.Attribute{ 53 | name: 'href' 54 | matcher: .exact 55 | value: '#test' 56 | } == css.Attribute{ 57 | name: 'href' 58 | value: '#test' 59 | } 60 | 61 | assert css.Attribute{ 62 | name: 'href' 63 | matcher: .contains 64 | value: '#test' 65 | } == css.Attribute{ 66 | name: 'href' 67 | value: 'test' 68 | } 69 | 70 | assert css.Attribute{ 71 | name: 'href' 72 | matcher: .starts_with 73 | value: '#test' 74 | } == css.Attribute{ 75 | name: 'href' 76 | value: '#te' 77 | } 78 | 79 | assert css.Attribute{ 80 | name: 'href' 81 | matcher: .ends_with 82 | value: '#test' 83 | } == css.Attribute{ 84 | name: 'href' 85 | value: 'st' 86 | } 87 | 88 | mut s1 := []css.Selector{} 89 | mut s2 := []css.Selector{} 90 | 91 | // [href="#test"] {} 92 | s1 = [css.Attribute{ 93 | name: 'href' 94 | matcher: .exact 95 | value: '#test' 96 | }] 97 | // 98 | s2 = [css.Type('a'), css.Attribute{ 99 | name: 'href' 100 | value: '#test' 101 | }] 102 | 103 | assert s1.matches(s2) == true 104 | 105 | // [data-test="wot"] {} 106 | s1 = [css.Attribute{ 107 | name: 'data-test' 108 | matcher: .exact 109 | value: 'wot' 110 | }] 111 | // 112 | s2 = [css.Type('a'), css.Attribute{ 113 | name: 'data-test' 114 | }] 115 | assert s1.matches(s2) == false 116 | } 117 | 118 | fn test_not() { 119 | mut s1 := []css.Selector{} 120 | mut s2 := []css.Selector{} 121 | 122 | // :not(.c), .a.b.c 123 | s1 = [css.PseudoClass{ 124 | name: 'not' 125 | children: [css.Class('c')] 126 | }] 127 | s2 = [css.Class('a'), css.Class('b'), css.Class('c')] 128 | 129 | assert s1.matches(s2) == false 130 | 131 | s2.pop() 132 | // :not(.c), .a.b 133 | assert s1.matches(s2) == true 134 | } 135 | -------------------------------------------------------------------------------- /css/tests/specificity_test.v: -------------------------------------------------------------------------------- 1 | import css 2 | 3 | fn test_specificity() { 4 | s := css.Specificity.from_selectors([ 5 | css.Id(''), 6 | css.Class(''), 7 | css.Type(''), 8 | css.Combinator(''), 9 | css.Id(''), 10 | css.Attribute{}, 11 | css.Attribute{}, 12 | // should be ignored 13 | css.Type('*'), 14 | ]) 15 | 16 | assert s.str() == '2-3-1' 17 | } 18 | 19 | fn test_pseudo_selectors() { 20 | s := css.Specificity.from_selectors([ 21 | // should be ignored 22 | css.PseudoClass{ 23 | name: 'where' 24 | children: [css.Id('')] 25 | }, 26 | css.PseudoClass{ 27 | name: 'is' 28 | }, 29 | css.PseudoClass{ 30 | name: 'has' 31 | }, 32 | // should not add to specificity, but children should count 33 | css.PseudoClass{ 34 | name: 'not' 35 | children: [css.Id('')] 36 | }, 37 | css.PseudoElement{ 38 | name: 'what' 39 | children: [css.Type('')] 40 | }, 41 | ]) 42 | 43 | assert s.str() == '1-0-2' 44 | } 45 | 46 | fn test_comparison() { 47 | // 1-2-0 48 | mut s1 := css.Specificity.from_selectors([css.Id(''), css.Class(''), css.Class('')]) 49 | 50 | // 1-2-1 51 | mut s2 := css.Specificity.from_selectors([css.Id(''), css.Class(''), css.Class(''), css.Type('')]) 52 | 53 | // 1-2-1 > 1-2-0 54 | assert s2 > s1 55 | 56 | // s1 + 1-0-0 = 2-2-0 57 | s1 += css.Specificity.from_selectors([css.Id('')]) 58 | 59 | // 2-2-0 > 1-2-0from_selector({ 60 | assert s1 > s2 61 | 62 | // s1 + 0-0-1 = 2-2-1 63 | s1 += css.Specificity.from_selectors([css.Type('')]) 64 | 65 | // s2 + 1-0-0 = 2-2-1 66 | s2 += css.Specificity.from_selectors([css.Id('')]) 67 | 68 | // 2-2-1 = 2-2-1 69 | assert s1 == s2 70 | } 71 | 72 | fn test_comparison_inverted() { 73 | // 0-2-0 74 | mut s1 := css.Specificity.from_selectors([css.Class(''), css.Class('')]) 75 | 76 | // 0-1-2 77 | mut s2 := css.Specificity.from_selectors([css.Class(''), css.Type(''), css.Type('')]) 78 | 79 | assert s1 > s2 80 | } 81 | -------------------------------------------------------------------------------- /css/token/pos.v: -------------------------------------------------------------------------------- 1 | module token 2 | 3 | pub struct Pos { 4 | pub: 5 | source string // the filename, if a string was passed it is "unkown" 6 | len int // length of the literal in the source 7 | line_nr int // the line number in the source where the token occured 8 | pos int // the position of the token in scanner text 9 | col int // the column in the source where the token occured 10 | pub mut: 11 | // TODO: remove?? 12 | last_line int // the line number where the ast object ends (used by vfmt) 13 | } 14 | 15 | pub fn (p &Pos) str() string { 16 | return '${p.line_nr + 1}:${p.col + 1}, len: ${p.len}' 17 | } 18 | 19 | @[unsafe] 20 | pub fn (mut p Pos) free() { 21 | } 22 | 23 | pub fn (p Pos) line_str() string { 24 | return '{l: ${p.line_nr + 1:5}, c: ${p.col:3}, p: ${p.pos:5}, ll: ${p.last_line + 1:5}}' 25 | } 26 | 27 | pub fn (pos Pos) extend(end Pos) Pos { 28 | return Pos{ 29 | ...pos 30 | len: end.pos - pos.pos + end.len 31 | last_line: end.last_line 32 | } 33 | } 34 | 35 | pub fn (pos Pos) extend_with_last_line(end Pos, last_line int) Pos { 36 | return Pos{ 37 | len: end.pos - pos.pos + end.len 38 | line_nr: pos.line_nr 39 | pos: pos.pos 40 | col: pos.col 41 | last_line: last_line - 1 42 | } 43 | } 44 | 45 | pub fn (mut pos Pos) update_last_line(last_line int) { 46 | pos.last_line = last_line - 1 47 | } 48 | 49 | @[inline] 50 | pub fn (tok &Token) pos() Pos { 51 | return Pos{ 52 | len: tok.len 53 | line_nr: tok.line_nr - 1 54 | pos: tok.pos 55 | last_line: tok.line_nr - 1 56 | col: tok.col - 1 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /css/token/token.v: -------------------------------------------------------------------------------- 1 | module token 2 | 3 | @[minify] 4 | pub struct Token { 5 | pub: 6 | kind Kind // the token number/enum; for quick comparisons 7 | lit string // literal representation of the token 8 | line_nr int // the line number in the source where the token occurred 9 | col int // the column in the source where the token occurred 10 | // name_idx int // name table index for O(1) lookup 11 | pos int // the position of the token in scanner text 12 | len int // length of the literal 13 | tidx int // the index of the token 14 | pub mut: 15 | // metadata used for strings to indicate whether the token has single or double quotes 16 | meta string 17 | } 18 | 19 | pub fn (t &Token) str() string { 20 | if t.lit.len != 0 { 21 | return '${t.kind} = "${t.lit}"' 22 | } else { 23 | return t.kind.str() 24 | } 25 | } 26 | 27 | pub enum Kind { 28 | unkown 29 | name 30 | eof 31 | number // 123 32 | string // 'foo' 33 | plus // + 34 | minus // - 35 | mul // * 36 | div // / 37 | gt // > 38 | comma // , 39 | semicolon // ; 40 | dot // . 41 | hash // # 42 | colon // : 43 | key_at // @ 44 | lcbr // { 45 | rcbr // } 46 | lpar // ( 47 | rpar // ) 48 | lsbr // [ 49 | rsbr // ] 50 | comment 51 | equal // = 52 | carrot // ^ 53 | dollar // $ 54 | exclamation // ! 55 | percentage // % 56 | color // ffffff 57 | } 58 | -------------------------------------------------------------------------------- /css/types.v: -------------------------------------------------------------------------------- 1 | module css 2 | 3 | import css.ast 4 | import css.datatypes 5 | 6 | pub type ColorValue = datatypes.Color | string 7 | 8 | // value between 0.0 and 1.0 9 | pub type AlphaValue = Keyword | f64 10 | pub type DimensionValue = Keyword 11 | | datatypes.CalcSum 12 | | datatypes.Length 13 | | datatypes.Percentage 14 | | f64 15 | 16 | pub type Keyword = string 17 | 18 | pub type Image = Gradient | Keyword | Url 19 | 20 | pub type TextCombineUprightDigits = int 21 | pub type TextCombineUpright = Keyword | TextCombineUprightDigits 22 | 23 | // text-overflow: ellipsis "[..]"; will become `css.TextOverflow(css.TextEllipsis('[..]'))` 24 | pub type TextEllipsis = string 25 | pub type TextOverflow = Keyword | TextEllipsis 26 | 27 | pub type ShadowValue = Keyword | Shadow 28 | 29 | pub type BorderLineStyle = Keyword | datatypes.LineStyle 30 | 31 | pub type FlexSize = Keyword | f64 32 | pub type FlexDirection = Keyword | datatypes.FlexDirectionKind 33 | pub type FlexWrap = Keyword | datatypes.FlexWrapKind 34 | 35 | pub type FontStretch = Keyword | datatypes.FontStretchKind | datatypes.Percentage 36 | pub type FontWeight = Keyword | int 37 | pub type FontFamily = []string 38 | 39 | pub type Value = AlphaValue 40 | | Background 41 | | Border 42 | | BorderColors 43 | | BorderLineStyle 44 | | BorderRadius 45 | | BorderStyles 46 | | ColorValue 47 | | DimensionValue 48 | | FlexBox 49 | | FlexDirection 50 | | FlexSize 51 | | FlexWrap 52 | | Font 53 | | FontFamily 54 | | FontStretch 55 | | FontWeight 56 | | FourDimensions 57 | | Gap 58 | | Gradient 59 | | Image 60 | | Keyword 61 | | Overflow 62 | | Shadow 63 | | SingleBorder 64 | | SingleBorderRadius 65 | | Text 66 | | TextCombineUpright 67 | | TextOverflow 68 | | string 69 | 70 | pub struct Attribute { 71 | pub mut: 72 | name string 73 | matcher ast.AttributeMatchType 74 | value ?string 75 | } 76 | 77 | pub fn (a Attribute) str() string { 78 | mut s := '[${a.name}' 79 | if v := a.value { 80 | s += match a.matcher { 81 | .@none { '' } 82 | .exact { '="${v}"' } 83 | .contains { '*="${v}"' } 84 | .starts_with { '^="${v}"' } 85 | .ends_with { '$="${v}"' } 86 | } 87 | } 88 | s += ']' 89 | return s 90 | } 91 | 92 | pub fn (a Attribute) == (b Attribute) bool { 93 | if a.name != b.name { 94 | return false 95 | } 96 | 97 | if va := a.value { 98 | vb := b.value or { return false } 99 | 100 | return match a.matcher { 101 | .@none { 102 | // should never reach this 103 | false 104 | } 105 | .exact { 106 | va == vb 107 | } 108 | .contains { 109 | va.contains(vb) 110 | } 111 | .starts_with { 112 | va.starts_with(vb) 113 | } 114 | .ends_with { 115 | va.ends_with(vb) 116 | } 117 | } 118 | } 119 | // a[href] 120 | return true 121 | } 122 | 123 | pub type Class = string 124 | 125 | pub fn (c Class) str() string { 126 | return '.' + c 127 | } 128 | 129 | pub type Combinator = string 130 | 131 | pub fn (cm Combinator) str() string { 132 | if cm != ' ' { 133 | return ' ' + cm + ' ' 134 | } else { 135 | return ' ' 136 | } 137 | } 138 | 139 | pub type Id = string 140 | 141 | pub fn (id Id) str() string { 142 | return '#' + id 143 | } 144 | 145 | pub type Type = string 146 | 147 | pub fn (typ Type) str() string { 148 | return typ 149 | } 150 | 151 | pub struct PseudoClass { 152 | pub mut: 153 | name string 154 | children []Selector 155 | } 156 | 157 | pub fn (pc PseudoClass) str() string { 158 | mut s := ':${pc.name}' 159 | if pc.children.len > 0 { 160 | s += '(${pc.children})' 161 | } 162 | return s 163 | } 164 | 165 | pub struct PseudoElement { 166 | pub mut: 167 | name string 168 | children []Selector 169 | } 170 | 171 | pub fn (pe PseudoElement) str() string { 172 | mut s := '::${pe.name}' 173 | if pe.children.len > 0 { 174 | s += '(${pe.children})' 175 | } 176 | return s 177 | } 178 | 179 | pub type Selector = Attribute | Class | Combinator | Id | PseudoClass | PseudoElement | Type 180 | 181 | pub fn (selectors []Selector) str() string { 182 | mut res := '' 183 | for s in selectors { 184 | res += match s { 185 | Class { s.str() } 186 | Id { s.str() } 187 | Type { s.str() } 188 | Combinator { s.str() } 189 | PseudoClass { s.str() } 190 | PseudoElement { s.str() } 191 | Attribute { s.str() } 192 | } 193 | } 194 | return res 195 | } 196 | 197 | // check if `other_selectors` is a subset of `current_selectors` 198 | // matches the part of `current_selectors` after the last Combinator 199 | pub fn (current_selectors []Selector) matches(other_selectors []Selector) bool { 200 | // last_selectors are all selectors that come after the last Combinator 201 | // in `current_selectors` 202 | mut last_selectors := []Selector{} 203 | for s in current_selectors { 204 | if s is Combinator { 205 | last_selectors.clear() 206 | } else { 207 | last_selectors << s 208 | } 209 | } 210 | 211 | for s in last_selectors { 212 | match s { 213 | PseudoClass { 214 | if s.name == 'not' { 215 | if s.children.matches(other_selectors) { 216 | return false 217 | } 218 | } 219 | // TODO: :is 220 | } 221 | PseudoElement { 222 | // pseudo elements should be matched in combination with a DOM 223 | return true 224 | } 225 | Attribute { 226 | for a in other_selectors { 227 | if a is Attribute && s != a { 228 | return false 229 | } 230 | } 231 | } 232 | else { 233 | if s !in other_selectors { 234 | return false 235 | } 236 | } 237 | } 238 | } 239 | return true 240 | } 241 | 242 | // Specificity represents CSS specificity 243 | // https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity 244 | pub struct Specificity { 245 | pub mut: 246 | col1 int 247 | col2 int 248 | col3 int 249 | } 250 | 251 | pub fn (s &Specificity) str() string { 252 | return '${s.col1}-${s.col2}-${s.col3}' 253 | } 254 | 255 | pub fn Specificity.from_selectors(selectors []Selector) Specificity { 256 | mut s := Specificity{} 257 | 258 | for current_selector in selectors { 259 | match current_selector { 260 | Id { 261 | s.col1++ 262 | } 263 | Attribute, Class { 264 | s.col2++ 265 | } 266 | PseudoClass { 267 | if current_selector.name == 'where' { 268 | // :where doesn't count for specificity, same applies for its children 269 | continue 270 | } else if current_selector.name !in ['is', 'has', 'not'] { 271 | // :is, :has and :not don't count for specificity, but their children do 272 | s.col2++ 273 | } 274 | s += Specificity.from_selectors(current_selector.children) 275 | } 276 | Type { 277 | if current_selector != '*' { 278 | // the universal selector does not count for specificity 279 | s.col3++ 280 | } 281 | } 282 | PseudoElement { 283 | s.col3++ 284 | s += Specificity.from_selectors(current_selector.children) 285 | } 286 | else {} 287 | } 288 | } 289 | 290 | return s 291 | } 292 | 293 | fn (a Specificity) == (b Specificity) bool { 294 | return a.col1 == b.col1 && a.col2 == b.col2 && a.col3 == b.col3 295 | } 296 | 297 | fn (a Specificity) < (b Specificity) bool { 298 | if a.col1 < b.col1 { 299 | return true 300 | } else if a.col1 == b.col1 { 301 | if a.col2 < b.col2 { 302 | return true 303 | } else if a.col2 == b.col2 { 304 | return a.col3 < b.col3 305 | } 306 | } 307 | return false 308 | } 309 | 310 | fn (a Specificity) + (b Specificity) Specificity { 311 | return Specificity{ 312 | col1: a.col1 + b.col1 313 | col2: a.col2 + b.col2 314 | col3: a.col3 + b.col3 315 | } 316 | } 317 | 318 | fn (a Specificity) - (b Specificity) Specificity { 319 | return Specificity{ 320 | col1: a.col1 - b.col1 321 | col2: a.col2 - b.col2 322 | col3: a.col3 - b.col3 323 | } 324 | } 325 | 326 | pub struct RawValue { 327 | pub mut: 328 | value Value 329 | important bool 330 | } 331 | 332 | // Rule represents a css rule with only a single selector 333 | pub struct Rule { 334 | pub mut: 335 | specificity Specificity 336 | selectors []Selector 337 | declarations map[string]RawValue 338 | } 339 | 340 | @[inline] 341 | pub fn (r Rule) matches(selectors []Selector) bool { 342 | return r.selectors.matches(selectors) 343 | } 344 | 345 | @[inline] 346 | pub fn (rules []Rule) get_matching(selectors []Selector) []Rule { 347 | return rules.filter(it.matches(selectors)) 348 | } 349 | 350 | pub fn (rules []Rule) get_styles() map[string]Value { 351 | // TODO: merge grouped properties together e.g. `background` gets split 352 | // into 'background-color', 'background-width' etc. 353 | // OR combine them from `background-color` to `background` only 354 | mut styles := map[string]Value{} 355 | mut importants := map[string]bool{} 356 | 357 | // TODO: reduce iterations 358 | for rule in rules { 359 | for property, value in rule.declarations { 360 | if !value.important && importants[property] == false { 361 | set_grouped(property, value.value, mut styles) 362 | } else if value.important { 363 | set_grouped(property, value.value, mut styles) 364 | importants[property] = true 365 | } 366 | } 367 | } 368 | 369 | return styles 370 | } 371 | -------------------------------------------------------------------------------- /css/util/util.v: -------------------------------------------------------------------------------- 1 | module util 2 | 3 | import css 4 | import css.ast 5 | import css.checker 6 | import css.gen 7 | import css.parser 8 | import css.pref 9 | import os 10 | 11 | // parse_file returns `file` in a stylesheet and fills `table` with the css rules 12 | pub fn parse_file(file string, strict bool, mut table ast.Table) !&ast.StyleSheet { 13 | mut prefs := pref.Preferences{ 14 | is_strict: strict 15 | } 16 | 17 | mut p := parser.Parser.new(prefs) 18 | p.table = table 19 | result := p.parse_file(file) 20 | p.table.sort_rules() 21 | 22 | // println('== Table: ===============================') 23 | // println(p.table) 24 | // println('== StyleSheet: ==========================') 25 | 26 | return result 27 | } 28 | 29 | // minify_file doesn't validate the css and only writes a minified version of the file 30 | pub fn minify_file(src_file string, out_file string) ! { 31 | prefs := pref.Preferences{} 32 | mut p := parser.Parser.new(prefs) 33 | p.table = &ast.Table{} 34 | tree := p.parse_file(src_file) 35 | 36 | minified := gen.generate(tree, gen.GenOptions{ 37 | minify: true 38 | }, prefs) 39 | os.write_file(out_file, minified)! 40 | } 41 | 42 | // minify_file doesn't validate the css and only writes a prettified version of the file 43 | pub fn prettify_file(src_file string, out_file string) ! { 44 | prefs := pref.Preferences{} 45 | mut p := parser.Parser.new(prefs) 46 | p.table = &ast.Table{} 47 | tree := p.parse_file(src_file) 48 | 49 | minified := gen.generate(tree, gen.GenOptions{ 50 | minify: false 51 | }, prefs) 52 | os.write_file(out_file, minified)! 53 | } 54 | 55 | pub fn parse_stylesheet_from_text(src string, prefs pref.Preferences) ![]css.Rule { 56 | mut table := &ast.Table{} 57 | 58 | mut p := parser.Parser.new(prefs) 59 | p.table = table 60 | tree := p.parse_text(src) 61 | 62 | rules := checker.validate(tree, mut table, prefs)! 63 | 64 | return rules 65 | } 66 | 67 | pub fn parse_stylesheet(css_file string, prefs pref.Preferences) ![]css.Rule { 68 | mut table := &ast.Table{} 69 | 70 | mut p := parser.Parser.new(prefs) 71 | p.table = table 72 | tree := p.parse_file(css_file) 73 | 74 | rules := checker.validate(tree, mut table, prefs)! 75 | 76 | return rules 77 | } 78 | -------------------------------------------------------------------------------- /examples/simple.css: -------------------------------------------------------------------------------- 1 | 2 | .test#what { 3 | opacity: 0.5; 4 | } 5 | 6 | #what { 7 | color: red; 8 | } 9 | 10 | #id, .test { 11 | /* add `!important` to see the difference */ 12 | color: green; 13 | } 14 | -------------------------------------------------------------------------------- /examples/simple.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import casper64.css 4 | import casper64.css.util as css_util 5 | import os 6 | 7 | fn main() { 8 | os.chdir(os.dir(@FILE))! 9 | // get all rules from `simple.css` 10 | mut rules := css_util.parse_stylesheet('simple.css')! 11 | 12 | // the selector representation for the element

13 | mut selector := [css.Selector(css.Type('p')), css.Class('test'), css.Id('what')] 14 | 15 | // filter all rules that match `selector` 16 | matching_rules := rules.get_matching(selector) 17 | // build a map of styles from the matching rules 18 | styles := matching_rules.get_styles() 19 | 20 | println('final styles for selector "${selector}":') 21 | println(styles) 22 | } 23 | -------------------------------------------------------------------------------- /progress/parser.md: -------------------------------------------------------------------------------- 1 | # CSS Specs implementation progress (Parser) 2 | 3 | - [X] Comments 4 | 5 | ## Selectors 6 | 7 | - [X] Attribute Selectors (`button[type="submit"]`) 8 | - [X] ID Selectors (`#id`) 9 | - [X] Class Selectors (`.class`) 10 | - [ ] Nested Selectors (yes it is actually build into CSS!) 11 | - [X] Type Selectors `p` 12 | - [*] Univseral Selectors (`*`) 13 | - [X] Selector Lists `p, div, .test` 14 | - [X] Selector combinators `.test p`, `.test > p` 15 | 16 | ## Properties 17 | 18 | - [X] Dimensions `100px` 19 | - [X] Strings `before: 'test'` 20 | - [X] Numbers: `10` 21 | - [X] Functions: `rgb(0, 0, 0)` 22 | - [X] Parentheses `calc(100vw - (10px + 20em))` 23 | - [X] `!important` 24 | 25 | ## Pseudo stuff 26 | 27 | - [X] Psuedo classes 28 | - [X] Pseudo elements 29 | 30 | ## At-rules 31 | - [X] `@charset` 32 | - [ ] `@color-profile` 33 | - [ ] `@container` 34 | - [ ] `@counter-style` 35 | - [ ] `@font-face` 36 | - [ ] `@import` 37 | - [X] `@keyframes` 38 | - [X] `@layer` 39 | - [X] `@media` 40 | - [ ] `@property` -------------------------------------------------------------------------------- /tests/bootstrap_test.v: -------------------------------------------------------------------------------- 1 | import css.ast 2 | import css.parser 3 | import css.pref 4 | 5 | const css_file = '${@VMODROOT}/tests/testdata/bootstrap.css' 6 | const min_css_file = '${@VMODROOT}/tests/testdata/bootstrap.min.css' 7 | 8 | fn test_parser_no_errors() { 9 | mut p := parser.Parser.new(pref.Preferences{ 10 | suppress_output: true 11 | is_strict: true 12 | }) 13 | p.table = &ast.Table{} 14 | p.parse_file(css_file) 15 | 16 | assert p.has_errored == false 17 | } 18 | 19 | fn test_parser_no_errors_minified() { 20 | mut p := parser.Parser.new(pref.Preferences{ 21 | suppress_output: true 22 | }) 23 | p.table = &ast.Table{} 24 | p.parse_file(min_css_file) 25 | 26 | assert p.has_errored == false 27 | } 28 | -------------------------------------------------------------------------------- /tests/testdata/all_properties.css: -------------------------------------------------------------------------------- 1 | /* The goal is to support at least all the properties below */ 2 | 3 | .property-list { 4 | align-content: center; 5 | align-items: center; 6 | align-self: center; 7 | all: initial; 8 | /* animation: 1s ease-in forwards infinite; 9 | animation-delay: .2s; 10 | animation-direction: reverse; 11 | animation-duration: 100ms; 12 | animation-fill-mode: none; 13 | animation-iteration-count: inherit; 14 | animation-name: bounce; 15 | animation-play-state: running; 16 | animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1); */ 17 | backface-visibility: hidden; 18 | /* background: linear-gradient(to left, #ffffff, black); */ 19 | background-attachment: fixed; 20 | background-blend-mode: multiply; 21 | background-clip: content-box; 22 | background-color: green; 23 | background-image: url('test.png'); 24 | background-image: linear-gradient(red, #0000ff); 25 | /* background-origin: padding-box; */ 26 | background-position: 50% 50% ; 27 | background-repeat: no-repeat; 28 | background-size: cover; 29 | border: 1px solid black; 30 | /* border-bottom: 1px solid black ; 31 | border-bottom-color: red; 32 | border-bottom-style: dotted; 33 | border-bottom-width: 2px; */ 34 | border-collapse: collapse; 35 | border-color: green; 36 | /* border-image: url('test.png'); 37 | border-image-outset: ; 38 | border-image-repeat: ; 39 | border-image-slice: ; 40 | border-image-source: ; 41 | border-image-width: ; 42 | border-left: ; 43 | border-left-color: ; 44 | border-left-style: ; 45 | border-left-width: ; 46 | border-radius: ; 47 | border-right: ; 48 | border-right-color: ; 49 | border-right-style: ; 50 | border-right-width: ; 51 | border-spacing: ; 52 | border-style: ; 53 | border-top: ; 54 | border-top-color: ; 55 | border-top-left: ; 56 | border-top-right: ; 57 | border-top-style: ; 58 | border-top-width: ; */ 59 | border-width: 5px; 60 | bottom: 20%; 61 | box-shadow: 4px 2px 2px rgb(0 0 0 / 12%); 62 | box-sizing: border-box; 63 | /* caption-side: ; 64 | caret-color: ; 65 | clear: ; 66 | clip: ; 67 | clip-path: ; */ 68 | color: black; 69 | /* column-count: ; 70 | column-fill: ; 71 | column-gap: ; 72 | column-rule: ; 73 | column-rule-color: ; 74 | column-rule-style: ; 75 | column-rule-width: ; 76 | column-span: ; 77 | column-width: ; 78 | columns: ; */ 79 | content: "test"; 80 | /* counter-increment: ; 81 | counter-reset: ; */ 82 | cursor: pointer; 83 | direction: ltr; 84 | display: block; 85 | /* empty-cells: ; */ 86 | filter: blur(2px); 87 | flex: content; 88 | flex-basis: 20px; 89 | flex-direction: column; 90 | flex-flow: row; 91 | flex-grow: 1; 92 | flex-shrink: 0; 93 | flex-wrap: nowrap; 94 | float: right; 95 | /* font: ; */ 96 | font-family: 'Courier New', Courier, monospace; 97 | /* font-kerning: ; */ 98 | font-size: 16px; 99 | /* font-size-adjust: ; 100 | font-stretch: ; */ 101 | font-style: italic; 102 | /* font-variant: ; */ 103 | font-weight: bold; 104 | /* grid: ; 105 | grid-area: ; */ 106 | grid-auto-columns: 10px; 107 | grid-auto-flow: dense; 108 | grid-auto-rows: 20px; 109 | grid-column: 1 / 3; 110 | grid-column-end: 3; 111 | grid-column-gap: 40px; 112 | grid-column-start: 1; 113 | grid-gap: 2em; 114 | grid-row: 2 / 4; 115 | grid-row-end: 4; 116 | grid-row-gap: 50px; 117 | grid-row-start: 2; 118 | /* grid-template: ; 119 | grid-template-areas: ; */ 120 | grid-template-columns: 100px 100px; 121 | grid-template-rows: repeat(auto-fit, 20px); 122 | height: 100vh; 123 | /* hyphens: ; */ 124 | justify-content: center; 125 | left: -20px; 126 | letter-spacing: 1.5; 127 | line-height: 1.5; 128 | /* list-style: ; 129 | list-style-image: ; 130 | list-style-position: ; */ 131 | list-style-type: circle; 132 | margin: 10px 20px 30px 40px; 133 | margin-bottom: 10px; 134 | margin-left: 20px; 135 | margin-right: 30px; 136 | margin-top: 40px; 137 | max-height: 200rem; 138 | max-width: 20vw; 139 | min-height: 40vh; 140 | min-width: 69%; 141 | /* object-fit: ; 142 | object-position: ; */ 143 | opacity: 0.9; 144 | /* order: ; */ 145 | outline: red 1px dotted; 146 | outline-color: invert; 147 | outline-offset: 1px; 148 | outline-style: solid; 149 | outline-width: 20%; 150 | overflow: auto; 151 | overflow-x: hidden; 152 | overflow-y: scroll; 153 | padding: 10px 20px 30px 40px; 154 | padding-bottom: 10px; 155 | padding-left: 20px; 156 | padding-right: 30px; 157 | padding-top: 40px; 158 | /* page-break-after: ; 159 | page-break-before: ; 160 | page-break-inside: ; 161 | perspective: ; 162 | perspective-origin: ; */ 163 | pointer-events: none; 164 | position: absolute; 165 | /* quotes: ; */ 166 | right: 20px; 167 | scroll-behavior: smooth; 168 | /* table-layout: ; */ 169 | text-align: right; 170 | /* text-align-last: ; */ 171 | text-decoration: blue underline wavy; 172 | text-decoration-color: blue; 173 | text-decoration-line: underline; 174 | text-decoration-style: wavy; 175 | text-indent: 40px; 176 | text-justify: inter-cluster; 177 | text-overflow: ellipsis; 178 | /* text-shadow: ; */ 179 | text-transform: uppercase; 180 | top: 1px; 181 | /* transform: ; 182 | transform-origin: ; 183 | transform-style: ; 184 | transition: ; 185 | transition-delay: ; 186 | transition-duration: ; 187 | transition-property: ; 188 | transition-timing-function: ; */ 189 | user-select: none; 190 | /* vertical-align: ; */ 191 | visibility: hidden; 192 | /* white-space: ; */ 193 | width: 100vw; 194 | word-break: break-all; 195 | /* word-spacing: ; */ 196 | word-wrap: break-word; 197 | writing-mode: vertical-rl; 198 | z-index: 4; 199 | } -------------------------------------------------------------------------------- /tests/testdata/order.css: -------------------------------------------------------------------------------- 1 | 2 | .test#what { 3 | opacity: 0.5; 4 | } 5 | 6 | #what { 7 | color: red; 8 | } 9 | 10 | #id, .test { 11 | color: green; 12 | } 13 | -------------------------------------------------------------------------------- /tests/testdata/simple.css: -------------------------------------------------------------------------------- 1 | 2 | .test, .other[href] { 3 | color: green; 4 | /* width: calc(100vw - (20px * 30px)); */ 5 | } 6 | 7 | .empty {} 8 | 9 | 10 | #id.mine, .test:not(#iaad.dsds) { 11 | background-color: green; 12 | /* border: 1px solid rgba(255 0 255 100); */ 13 | width: 100vw !important; 14 | } 15 | 16 | 17 | 18 | @layer test { 19 | .mine { 20 | width: 200px; 21 | } 22 | } -------------------------------------------------------------------------------- /tests/testdata/tokens.css: -------------------------------------------------------------------------------- 1 | 2 | .my-class#id#aaaaaa { 3 | color: red; 4 | width: 10em; 5 | } 6 | -------------------------------------------------------------------------------- /v.mod: -------------------------------------------------------------------------------- 1 | Module { 2 | name: 'css' 3 | description: 'A parser for CSS in V' 4 | version: '0.0.1' 5 | license: 'MIT' 6 | dependencies: [] 7 | } --------------------------------------------------------------------------------