OLD | NEW |
1 Sky Style Language | 1 Sky Style Language |
2 ================== | 2 ================== |
3 | 3 |
4 For now, the Sky style language is CSS with the following restrictions: | |
5 | |
6 - No combinators | |
7 - Only = and ~= attribute selectors | |
8 - Lots of other selectors removed // TODO(ianh): list them | |
9 - Floats removed | |
10 - Lots of other layout models removed // TODO(ianh): list them | |
11 | |
12 | |
13 Planed changes | 4 Planed changes |
14 -------------- | 5 -------------- |
15 | 6 |
16 Add //-to-end-of-line comments to be consistent with the script | 7 Add //-to-end-of-line comments to be consistent with the script |
17 language. | 8 language. |
18 | 9 |
19 Add a way to add new values, e.g. by default only support #RRGGBB | 10 |
20 colours (or maybe only rgba() colours), but provide a way to enable | 11 Style Parser |
21 CSS4-like "color(red rgb(+ #004400))" stuff. | 12 ------------ |
| 13 |
| 14 (this section is incomplete) |
| 15 |
| 16 ### Tokenisation |
| 17 |
| 18 |
| 19 #### Value parser |
| 20 |
| 21 |
| 22 ##### **Value** state |
| 23 |
| 24 If the current character is... |
| 25 |
| 26 * '``;``': Consume the character and exit the value parser |
| 27 successfully. |
| 28 |
| 29 * '``@``': Consume the character and switch to the **at** |
| 30 state. |
| 31 |
| 32 * '``#``': Consume the character and switch to the **hash** |
| 33 state. |
| 34 |
| 35 * '``$``': Consume the character and switch to the **dollar** |
| 36 state. |
| 37 |
| 38 * '``%``': Consume the character and switch to the **percent** |
| 39 state. |
| 40 |
| 41 * '``&``': Consume the character and switch to the **ampersand** |
| 42 state. |
| 43 |
| 44 * '``'``': Set _value_ to the empty string, consume the character, and |
| 45 switch to the **single-quoted string** state. |
| 46 |
| 47 * '``"``': Set _value_ to the empty string, consume the character, and |
| 48 switch to the **double-quoted string** state. |
| 49 |
| 50 * '``-``': Consume the character, and switch to the **negative |
| 51 integer** state. |
| 52 |
| 53 * '``0``'-'``9``': Set _value_ to the decimal value of the current |
| 54 character, consume the character, and switch to the **integer** |
| 55 state. |
| 56 |
| 57 * '``a``'-'``z``', '``A``'-'``Z``': Set _value_ to the current |
| 58 character, consume the character, and switch to the **identifier** |
| 59 state. |
| 60 |
| 61 * '``*``', '``^``', '``!``', '``?``', '``,``', '``/``', '``<``', |
| 62 '``[``', '``)``', '``>``', '``]``', '``+``': Emit a symbol token |
| 63 with the current character as the symbol, consume the character, and |
| 64 stay in this state. |
| 65 |
| 66 * Anything else: Consume the character and switch to the **error** |
| 67 state. |
| 68 |
| 69 |
| 70 ##### **At** state |
| 71 |
| 72 * '``0``'-'``9``', '``a``'-'``z``', '``A``'-'``Z``': Set _value_ to |
| 73 the current character, create a literal token with the unit set to |
| 74 ``@``, consume the character, and switch to the **literal** state. |
| 75 |
| 76 * Anything else: Emit a symbol token with ``@`` as the symbol, and |
| 77 switch to the **value** state without consuming the character. |
| 78 |
| 79 |
| 80 ##### **Hash** state |
| 81 |
| 82 * '``0``'-'``9``', '``a``'-'``z``', '``A``'-'``Z``': Set _value_ to |
| 83 the current character, create a literal token with the unit set to |
| 84 ``@``, consume the character, and switch to the **literal** state. |
| 85 |
| 86 * Anything else: Emit a symbol token with ``#`` as the symbol, and |
| 87 switch to the **value** state without consuming the character. |
| 88 |
| 89 |
| 90 ##### **Dollar** state |
| 91 |
| 92 * '``0``'-'``9``', '``a``'-'``z``', '``A``'-'``Z``': Set _value_ to |
| 93 the current character, create a literal token with the unit set to |
| 94 ``@``, consume the character, and switch to the **literal** state. |
| 95 |
| 96 * Anything else: Emit a symbol token with ``$`` as the symbol, and |
| 97 switch to the **value** state without consuming the character. |
| 98 |
| 99 |
| 100 ##### **Percent** state |
| 101 |
| 102 * '``0``'-'``9``', '``a``'-'``z``', '``A``'-'``Z``': Set _value_ to |
| 103 the current character, create a literal token with the unit set to |
| 104 ``@``, consume the character, and switch to the **literal** state. |
| 105 |
| 106 * Anything else: Emit a symbol token with ``%`` as the symbol, and |
| 107 switch to the **value** state without consuming the character. |
| 108 |
| 109 |
| 110 ##### **Ampersand** state |
| 111 |
| 112 * '``0``'-'``9``', '``a``'-'``z``', '``A``'-'``Z``': Set _value_ to |
| 113 the current character, create a literal token with the unit set to |
| 114 ``@``, consume the character, and switch to the **literal** state. |
| 115 |
| 116 * Anything else: Emit a symbol token with ``&`` as the symbol, and |
| 117 switch to the **value** state without consuming the character. |
| 118 |
| 119 |
| 120 ##### TODO(ianh): more states... |
| 121 |
| 122 |
| 123 ##### **Error** state |
| 124 |
| 125 If the current character is... |
| 126 |
| 127 * '``;``': Consume the character and exit the value parser in failure. |
| 128 |
| 129 * Anything else: Consume the character and stay in this state. |
| 130 |
| 131 |
| 132 |
| 133 Selectors |
| 134 --------- |
| 135 |
| 136 Sky Style uses whatever SelectorQuery. Maybe one day we'll make |
| 137 SelectorQuery support being extended to support arbitrary selectors, |
| 138 but for now, it supports: |
| 139 |
| 140 tagname |
| 141 #id |
| 142 .class |
| 143 [attrname] |
| 144 [attrname=value] |
| 145 :host |
| 146 |
| 147 These can be combined (without whitespace), with at most one tagname, |
| 148 as in: |
| 149 |
| 150 tagname[attrname]#id:host.class.class[attrname=value] |
| 151 |
| 152 In debug mode, giving two IDs, or the same selector twice (e.g. the |
| 153 same classname), or specifying other redundant or conflicting |
| 154 selectors (e.g. [foo][foo=bar], or [foo=bar][foo=baz]) will be |
| 155 flagged. |
| 156 |
| 157 Alternatively, a selector can be the following special value: |
| 158 |
| 159 @document |
| 160 |
| 161 |
| 162 Value Parser |
| 163 ------------ |
| 164 |
| 165 class StyleToken { |
| 166 constructor (String king, String value); |
| 167 readonly attribute String kind; |
| 168 // string |
| 169 // identifier |
| 170 // function (identifier + '(') |
| 171 // number |
| 172 // symbol (one of @#$%& if not immediately following numeric or preceding
alphanumeric, or one of *^!?,/<[)>]+ or, if not followed by a digit, -) |
| 173 // dimension (number + identifier or number + one of @#$%&) |
| 174 // literal (one of @#$%& + alphanumeric) |
| 175 readonly attribute String value; |
| 176 readonly attribute String unit; // for 'dimension' type, this is the punctua
tion or identifier that follows the number, for 'literal' type, this is the punc
tuation that precedes it |
| 177 } |
| 178 |
| 179 class TokenSource { |
| 180 constructor (Array<StyleToken> tokens); |
| 181 IteratorResult next(); |
| 182 TokenSourceBookmark getBookmark(); |
| 183 void rewind(TokenSourceBookmark bookmark); |
| 184 } |
| 185 class TokenSourceBookmark { |
| 186 constructor (); |
| 187 // TokenSource stores unforgeable state on this object using symbols or a we
akmap or some such |
| 188 } |
| 189 |
| 190 dictionary ParsedValue { |
| 191 any value = null; |
| 192 ValueResolver? resolver = null; |
| 193 Boolean relativeDimension = false; // if true, e.g. for % lengths, the callb
ack will be called again if an ancestor's dimensions change |
| 194 Painter? painter = null; |
| 195 } |
| 196 |
| 197 // best practice convention: if you're creating a property with needsPaint, yo
u should |
| 198 // create a new style value type for it so that it can set the paint callback
right; |
| 199 // you should never use such a style type when parsing another property |
| 200 |
| 201 callback any ParserCallback (TokenSource tokens); |
| 202 |
| 203 class StyleValueType { |
| 204 constructor (); |
| 205 void addParser(ParserCallback parser); |
| 206 any parse(TokenSource tokens, Boolean root = false); |
| 207 // for each parser callback that was registered, in reverse |
| 208 // order (most recently registered first), run these steps: |
| 209 // let bookmark = tokens.getBookmark(); |
| 210 // try { |
| 211 // let result = parser(tokens); |
| 212 // if (root) { |
| 213 // if (!tokens.next().done) |
| 214 // throw new Error(); |
| 215 // } |
| 216 // } except { |
| 217 // tokens.rewind(bookmark); |
| 218 // } |
| 219 // (root is set when you need to parse the entire token stream to be valid) |
| 220 } |
| 221 |
| 222 // note: if you define a style value type that uses other style value types, e
.g. a "length pair" that accepts two lengths, then |
| 223 // if any of the subtypes have a resolver, you need to make sure you have a re
solver that calls them to compute the final value |
| 224 |
| 225 dictionary PropertySettings { |
| 226 String name; |
| 227 StyleValueType type; // the output from the parser is coerced to a ParsedVal
ue |
| 228 Boolean inherits = false; |
| 229 any initialValue = null; |
| 230 Boolean needsLayout = false; |
| 231 Boolean needsPaint = false; |
| 232 } |
| 233 |
| 234 void registerProperty(PropertySettings propertySettings); |
| 235 // when you register a new property, document the format that is expected to
be cascaded |
| 236 // (the output from the propertySettings.type parser's ParsedValue.value fie
ld after the resolver, if any, has been called) |
| 237 |
| 238 // sky:core exports a bunch of style value types so that people can |
| 239 // extend them |
| 240 attribute StyleValueType PositiveLengthOrInfinityStyleValueType; |
| 241 attribute StyleValueType PositiveLengthOrAutoStyleValueType; |
| 242 attribute StyleValueType PositiveLengthStyleValueType; |
| 243 attribute StyleValueType DisplayStyleValueType; |
| 244 |
| 245 |
| 246 Inline Styles |
| 247 ------------- |
| 248 |
| 249 partial class Element { |
| 250 readonly attribute StyleDeclarationList style; |
| 251 } |
| 252 |
| 253 class StyleDeclarationList { |
| 254 constructor (); |
| 255 void add(StyleDeclaration styles); // O(1) // in debug mode, throws if the dic
tionary has any properties that aren't registered |
| 256 void remove(StyleDeclaration styles); // O(N) in number of declarations |
| 257 Array<StyleDeclaration> getDeclarations(); // O(N) in number of declarations |
| 258 } |
| 259 |
| 260 typedef StyleDeclaration Dictionary<ParsedValue>; |
| 261 |
| 262 |
| 263 Rule Matching |
| 264 ------------- |
| 265 |
| 266 partial class StyleElement { |
| 267 Array<Rule> getRules(); // O(N) in rules |
| 268 } |
| 269 |
| 270 class Rule { |
| 271 constructor (); |
| 272 attribute SelectorQuery selector; // O(1) |
| 273 attribute StyleDeclaration styles; // O(1) |
| 274 } |
| 275 |
| 276 Each frame, at some defined point relative to requestAnimationFrame(): |
| 277 - If a rule starts applying to an element, sky:core calls thatElement.style.add
(rule.styles); |
| 278 - If a rule stops applying to an element, sky:core calls thatElement.style.remo
ve(rule.styles); |
| 279 |
| 280 |
| 281 Cascade |
| 282 ------- |
| 283 |
| 284 For each Element, the StyleDeclarationList is conceptually flattened |
| 285 so that only the last declaration mentioning a property is left. |
| 286 |
| 287 Create the flattened render tree as a tree of StyleNode objects |
| 288 (described below). For each one, run the equivalent of the following |
| 289 code: |
| 290 |
| 291 var display = node.getProperty('display'); |
| 292 if (display) { |
| 293 node.layoutManager = new display(node, ownerManager); |
| 294 return true; |
| 295 } |
| 296 return false; |
| 297 |
| 298 If that code returns false, then that node an all its descendants must |
| 299 be dropped from the render tree. |
| 300 |
| 301 If any node is removed in this pass relative to the previous pass, and |
| 302 it has an ownerLayoutManager, then call |
| 303 ``node.ownerLayoutManager.release(node)`` |
| 304 ...to notify the layout manager that the node went away, then set the |
| 305 node's layoutManager and ownerLayoutManager attributes to null. |
| 306 |
| 307 callback any ValueResolver (any value, String propertyName, StyleNode node, Fl
oat containerWidth, Float containerHeight); |
| 308 |
| 309 class StyleNode { |
| 310 // this is generated before layout |
| 311 readonly attribute String text; |
| 312 readonly attribute Node? parentNode; |
| 313 readonly attribute Node? firstChild; |
| 314 readonly attribute Node? nextSibling; |
| 315 |
| 316 // access to the results of the cascade |
| 317 any getProperty(String name); |
| 318 // if there's a cached value, return it |
| 319 // otherwise, if there's an applicable ParsedValue, then |
| 320 // if it has a resolver: |
| 321 // call it |
| 322 // cache the value |
| 323 // if relativeDimension is true, then mark the value as provisional |
| 324 // return the value |
| 325 // otherwise use the ParsedValue's value; cache it; return it |
| 326 // otherwise, if the property is inherited and there's a parent: |
| 327 // get it from the parent; cache it; return it |
| 328 // otherwise, get the default value; cache it; return it |
| 329 |
| 330 readonly attribute Boolean needsLayout; // means that a property with needsL
ayout:true has changed on this node or one of its descendants |
| 331 readonly attribute LayoutManager layoutManager; |
| 332 |
| 333 readonly attribute LayoutManager ownerLayoutManager; // defaults to the pare
ntNode.layoutManager |
| 334 // if you are not the ownerLayoutManager, then ignore this StyleNode in la
yout() and paintChildren() |
| 335 // using walkChildren() does this for you |
| 336 |
| 337 readonly attribute Boolean needsPaint; // means that either needsLayout is t
rue or a property with needsPaint:true has changed on this node or one of its de
scendants |
| 338 |
| 339 // only the ownerLayoutManager can change these |
| 340 readonly attribute Float x; |
| 341 readonly attribute Float y; |
| 342 readonly attribute Float width; |
| 343 readonly attribute Float height; |
| 344 } |
| 345 |
| 346 The flattened tree is represented as a hierarchy of Node objects. For |
| 347 any element that only contains text node children, the "text" property |
| 348 is set accordingly. For elements with mixed text node and non-text |
| 349 node children, each run of text nodes is represented as a separate |
| 350 Node with the "text" property set accordingly and the styles set as if |
| 351 the Node inherited everything inheritable from its parent. |
| 352 |
| 353 |
| 354 Layout |
| 355 ------ |
| 356 |
| 357 sky:core registers 'display' as follows: |
| 358 |
| 359 { |
| 360 name: 'display', |
| 361 type: sky.DisplayStyleValueType, |
| 362 inherits: false, |
| 363 initialValue: sky.BlockLayoutManager, |
| 364 needsLayout: true, |
| 365 } |
| 366 |
| 367 The following API is then used to add new layout manager types to 'display': |
| 368 |
| 369 void registerLayoutManager(String displayValue, LayoutManagerConstructor? layo
utManager); |
| 370 |
| 371 sky:core by default registers: |
| 372 |
| 373 'block': sky.BlockLayoutManager |
| 374 'paragraph': sky.ParagraphLayoutManager |
| 375 'inline': sky.InlineLayoutManager |
| 376 'none': null |
| 377 |
| 378 |
| 379 Layout managers inherit from the following API: |
| 380 |
| 381 class LayoutManager { |
| 382 readonly attribute StyleNode node; |
| 383 constructor LayoutManager(StyleNode node); |
| 384 |
| 385 void take(StyleNode victim); // sets victim.ownerLayoutManager = this; |
| 386 // assert: victim hasn't been take()n yet during this layout |
| 387 // assert: victim.needsLayout == true |
| 388 // assert: an ancestor of victim has needsLayout == this (aka, victim is a
descendant of this.node) |
| 389 |
| 390 virtual void release(StyleNode victim); |
| 391 // called when the StyleNode was removed from the tree |
| 392 |
| 393 void setChildPosition(child, x, y); // sets child.x, child.y |
| 394 void setChildX(child, y); // sets child.x |
| 395 void setChildY(child, y); // sets child.y |
| 396 void setChildSize(child, width, height); // sets child.width, child.height |
| 397 void setChildWidth(child, width); // sets child.width |
| 398 void setChildHeight(child, height); // sets child.height |
| 399 // these set needsPaint on the node and on any node impacted by this (?) |
| 400 // for setChildSize/Width/Height: if the new dimension is different than t
he last assumed dimensions, and |
| 401 // any StyleNodes with an ownerLayoutManager==this have cached values for
getProperty() that are marked |
| 402 // as provisional, clear them |
| 403 |
| 404 Generator<StyleNode> walkChildren(); |
| 405 // returns a generator that iterates over the children, skipping any whose
ownerLayoutManager is not this |
| 406 |
| 407 void paint(RenderingSurface canvas); |
| 408 // set a clip rect on the canvas |
| 409 // call the painter of each property, in order they were registered, which
on this element has a painter |
| 410 // call this.paintChildren() |
| 411 // unset the clip |
| 412 |
| 413 virtual void paintChildren(RenderingSurface canvas); |
| 414 // just calls paint() for each child returned by walkChildren() whose need
sPaint is true |
| 415 |
| 416 void assumeDimensions(Float width, Float height); |
| 417 // sets the assumed dimensions for calls to getProperty() on StyleNodes th
at have this as an ownerLayoutManager |
| 418 // if the new dimension is different than the last assumed dimensions, and
any StyleNodes with an |
| 419 // ownerLayoutManager==this have cached values for getProperty() that are
marked as provisional, clear them |
| 420 // TODO(ianh): should we force this to match the input to layout(), when c
alled from inside layout() and when |
| 421 // layout() has a forced width and/or height? |
| 422 |
| 423 virtual LayoutValueRange getIntrinsicWidth(Float? defaultWidth = null); |
| 424 // returns min-width, width, and max-width, normalised, defaulting to valu
es given in LayoutValueRange |
| 425 // if argument is provided, it overrides width |
| 426 |
| 427 virtual LayoutValueRange getIntrinsicHeight(Float? defaultHeight = null); |
| 428 // returns min-height, height, and max-height, normalised, defaulting to v
alues given in LayoutValueRange |
| 429 // if argument is provided, it overrides height |
| 430 |
| 431 virtual Dimensions layout(Number? width, Number? height); |
| 432 // returns { } |
| 433 // the return value should include the final value for whichever of the wi
dth and height arguments that is null |
| 434 // TODO(ianh): should we just grab the width and height from assumeDimensi
ons()? |
| 435 |
| 436 } |
| 437 |
| 438 dictionary LayoutValueRange { |
| 439 // negative values here should be treated as zero |
| 440 Float minimum = 0; |
| 441 Float value = 0; // ideal desired width; if it's not in the range minimum ..
maximum then it overrides minimum and maximum |
| 442 (Float or Infinity) maximum = Infinity; |
| 443 } |
| 444 |
| 445 dictionary Dimensions { |
| 446 Float width = 0; |
| 447 Float height = 0; |
| 448 } |
| 449 |
| 450 |
| 451 Given a tree of StyleNode objects rooted at /node/, the application is |
| 452 rendered as follows: |
| 453 |
| 454 node.layoutManager.layout(screen.width, screen.height); |
| 455 node.layoutManager.paint(); |
| 456 |
| 457 |
| 458 |
| 459 Paint |
| 460 ----- |
| 461 |
| 462 callback void Painter (StyleNode node, RenderingSurface canvas); |
| 463 |
| 464 class RenderingSurface { |
| 465 // ... |
| 466 } |
| 467 |
| 468 |
| 469 Default Styles |
| 470 -------------- |
| 471 |
| 472 In the constructors for the default elements, they add to themselves |
| 473 StyleDeclaration objects as follows: |
| 474 |
| 475 * ``import`` |
| 476 * ``template`` |
| 477 * ``style`` |
| 478 * ``script`` |
| 479 * ``content`` |
| 480 * ``title`` |
| 481 These all add to themselves the same declaration with value: |
| 482 ``{ display: { value: null } }`` |
| 483 |
| 484 * ``img`` |
| 485 This adds to itself the declaration with value: |
| 486 ``{ display: { value: sky.ImageElementLayoutManager } }`` |
| 487 |
| 488 * ``span`` |
| 489 * ``a`` |
| 490 These all add to themselves the same declaration with value: |
| 491 ``{ display: { value: sky.InlineLayoutManager } }`` |
| 492 |
| 493 * ``iframe`` |
| 494 This adds to itself the declaration with value: |
| 495 ``{ display: { value: sky.IFrameElementLayoutManager } }`` |
| 496 |
| 497 * ``t`` |
| 498 This adds to itself the declaration with value: |
| 499 ``{ display: { value: sky.ParagraphLayoutManager } }`` |
| 500 |
| 501 * ``error`` |
| 502 This adds to itself the declaration with value: |
| 503 ``{ display: { value: sky.ErrorLayoutManager } }`` |
| 504 |
| 505 The ``div`` element doesn't have any default styles. |
| 506 |
| 507 These declarations are all shared between all the elements (so e.g. if |
| 508 you reach in and change the declaration that was added to a ``title`` |
| 509 element, you're going to change the styles of all the other |
| 510 default-hidden elements). |
OLD | NEW |