Specs: Initial hack at extensible style/layout

Review URL: https://codereview.chromium.org/716013002
diff --git a/sky/examples/style/block-layout.sky b/sky/examples/style/block-layout.sky
new file mode 100644
index 0000000..b303db0
--- /dev/null
+++ b/sky/examples/style/block-layout.sky
@@ -0,0 +1,74 @@
+SKY MODULE
+<import src="sky:core" as="sky"/>
+<!--
+ ! this module provides trivial vertical block layout
+ ! no margins, padding, borders, etc
+ !-->
+<script>
+ module.exports.BlockLayoutManager = class BlockLayoutManager extends sky.LayoutManager {
+   function layout(width, height) {
+     if (width == null)
+       width = this.getIntrinsicWidth().value;
+     let autoHeight = false;
+     if (height == null) {
+       height = 0;
+       autoHeight = true;
+     }
+     this.assumeDimensions(width, height);
+     let children = this.walkChildren();
+     let loop = children.next();
+     let y = 0;
+     while (!loop.done) {
+       let child = loop.value;
+       if (child.needsLayout) {
+         let dims = child.layoutManager.layout(width, null);
+         this.setChildSize(child, dims.width, dims.height);
+       }
+       this.setChildPosition(child, 0, y);
+       y += child.height;
+       loop = children.next();
+     }
+     if (autoHeight)
+       height = y;
+     return {
+       width: width,
+       height: height,
+     }
+   }
+   function getIntrinsicWidth() {
+     let width = this.node.getProperty('width');
+     if (typeof height != 'number') {
+       // e.g. width: auto
+       width = 0;
+       let children = this.walkChildren();
+       let loop = children.next();
+       while (!loop.done) {
+         let child = loop.value;
+         let childWidth = child.layoutManager.getIntrinsicWidth();
+         if (width < childWidth.value)
+           width = childWidth.value;
+         loop = children.next();
+       }
+     }
+     return super(width); // applies and provides our own min-width/max-width rules
+   }
+   function getIntrinsicHeight() {
+     let height = this.node.getProperty('height');
+     if (typeof height != 'number') {
+       // e.g. height: auto
+       height = 0;
+       let children = this.walkChildren();
+       let loop = children.next();
+       while (!loop.done) {
+         let child = loop.value;
+         let childHeight = child.layoutManager.getIntrinsicHeight();
+         if (height < childHeight.value)
+           height = childHeight.value;
+         loop = children.next();
+       }
+     }
+     return super(height); // applies and provides our own min-width/max-width rules
+   }
+ }
+ sky.registerLayoutManager('block', module.exports.BlockLayoutManager);
+</script>
diff --git a/sky/examples/style/hex-layout.sky b/sky/examples/style/hex-layout.sky
new file mode 100644
index 0000000..ca286f0
--- /dev/null
+++ b/sky/examples/style/hex-layout.sky
@@ -0,0 +1,146 @@
+#!mojo mojo:sky
+<import src="sky:core" as="sky"/>
+<script>
+ class BeehiveLayoutManager extends sky.LayoutManager {
+   function layout(width, height) {
+     if (width == null)
+       width = this.getIntrinsicWidth().value;
+     let autoHeight = false;
+     if (height == null) {
+       height = 0;
+       autoHeight = true;
+     }
+     this.assumeDimensions(width, height);
+     let cellCount = this.node.getProperty('beehive-count');
+     let cellDim = width / cellCount;
+     let children = this.walkChildren();
+     let loop = children.next();
+     let x = 0;
+     let y = 0;
+     while (!loop.done) {
+       let child = loop.value;
+       if (child.needsLayout) {
+         child.layoutManager.layout(cellDim, cellDim);
+         // we ignore the size the child reported from layout(), and force it to the cell dimensions
+         this.setChildSize(child, cellDim, cellDim);
+       }
+       this.setChildPosition(child, x * cellDim + (y % 2) * cellDim/2, y * 3/4 * cellDim);
+       x += 1;
+       if (x > cellCount) {
+         y += 1;
+         x = 0;
+       }
+       loop = children.next();
+     }
+     if (height == 0)
+       height = (1 + y * 3/4) * cellDim;
+     return {
+       width: width,
+       height: height,
+     }
+   }
+   function getIntrinsicWidth() {
+     // this is the logic that LayoutManager.getIntrinsicWidth() has by default
+     // shown here because I wrote it before realising it should be the default
+     let width = this.node.getProperty('width');
+     if (typeof width != 'number')
+       width = 0;
+     let minWidth = this.node.getProperty('min-width');
+     if (typeof width != 'number')
+       minWidth = 0;
+     let maxWidth = this.node.getProperty('max-width');
+     if (typeof width != 'number')
+       maxWidth = Infinity;
+     if (maxWidth < minWidth)
+       maxWidth = minWidth;
+     if (width > maxWidth)
+       width = maxWidth;
+     if (width < minWidth)
+       width = minWidth;
+     return {
+       minimum: minWidth,
+       value: width,
+       maximum: maxWidth,
+     };
+   }
+   function getIntrinsicHeight() {
+     let height = this.node.getProperty('height');
+     if (typeof height != 'number') {
+       // e.g. height: auto
+       width = this.getIntrinsicWidth().value;
+       let cellCount = this.node.getProperty('beehive-count');
+       let cellDim = width / cellCount;
+       let children = this.walkChildren();
+       let loop = children.next();
+       let childCount = 0;
+       while (!loop.done) {
+         childCount += 1;
+         loop.next();
+       }
+       if (childCount > 0)
+         height = cellDim * (1/4 + Math.ceil(childCount / cellCount) * 3/4);
+       else
+         height = 0;
+     }
+     return super(height); // does the equivalent of getIntrinsicWidth() above, applying min-height etc
+   }
+   function paintChildren(RenderingSurface canvas) {
+     let width = this.node.width;
+     let cellCount = this.node.getProperty('beehive-count');
+     let cellDim = width / cellCount;
+     let children = this.walkChildren();
+     let loop = children.next();
+     while (!loop.done) {
+       let child = loop.value;
+       if (child.needsPaint) {
+         canvas.save();
+         try {
+           canvas.beginPath();
+           canvas.moveTo(child.x, child.y + cellDim/4);
+           canvas.lineTo(child.x + cellDim/2, child.y);
+           canvas.lineTo(child.x + cellDim, child.y + cellDim/4);
+           canvas.lineTo(child.x + cellDim, child.y + 3*cellDim/4);
+           canvas.lineTo(child.x + cellDim/2, child.y + cellDim);
+           canvas.moveTo(child.x, child.y + 3*cellDim/4);
+           canvas.closePath();
+           canvas.clip();
+           child.paint(canvas);
+         } finally {
+           canvas.restore();
+         }
+       }
+       loop = children.next();
+     }
+   }
+ }
+ sky.registerLayoutManager('beehive', BeehiveLayoutManager);
+ let BeehiveCountStyleValueType = new StyleValueType();
+ BeehiveCountStyleValueType.addParser((tokens) => {
+   let token = tokens.next();
+   if (token.done) throw new Error();
+   if (token.value.kind != 'number') throw new Error();
+   if (token.value.value <= 0) throw new Error();
+   if (Math.trunc(token.value.value) != token.value.value) throw new Error();
+   let result = token.value.value;
+   if (!token.next().done) throw new Error();
+   return result;
+ });
+ sky.registerProperty({
+   name: 'beehive-count',
+   type: BeehiveCountStyleValueType,
+   inherits: true,
+   initialValue: 5,
+   needsLayout: true,
+ });
+</script>
+<style>
+ div { display: beehive; beehive-count: 3; }
+</style>
+<div>
+ <t>Hello</t>
+ <t>World</t>
+ <t>How</t>
+ <t>Are</t>
+ <t>You</t>
+ <t>Today?</t>
+</div>
diff --git a/sky/examples/style/sky-core-styles.sky b/sky/examples/style/sky-core-styles.sky
new file mode 100644
index 0000000..640236a
--- /dev/null
+++ b/sky/examples/style/sky-core-styles.sky
@@ -0,0 +1,143 @@
+SKY MODULE
+<!-- this is part of sky:core -->
+<script>
+ // "internals" is an object only made visible to this module that exports stuff implemented in C++
+ module.exports.registerProperty = internals.registerProperty;
+ internals.registerLayoutManager('none', null);
+ module.exports.LayoutManager = internals.LayoutManager;
+ module.exports.InlineLayoutManager = internals.InlineLayoutManager;
+ internals.registerLayoutManager('inline', internals.InlineLayoutManager);
+ module.exports.ParagraphLayoutManager = internals.ParagraphLayoutManager;
+ internals.registerLayoutManager('paragraph', internals.ParagraphLayoutManager);
+ module.exports.BlockLayoutManager = internals.BlockLayoutManager;
+ internals.registerLayoutManager('block', internals.BlockLayoutManager);
+
+ let displayTypes = new Map();
+ module.exports.registerLayoutManager = function registerLayoutManager(displayValue, layoutManagerConstructor) {
+   // TODO(ianh): apply rules for type-checking displayValue is a String
+   // TODO(ianh): apply rules for type-checking layoutManagerConstructor implements the LayoutManagerConstructor interface (or is null)
+   if (displayTypes.has(displayValue))
+     throw new Error();
+   displayTypes.set(displayValue, layoutManagerConstructor);
+ };
+
+ module.exports.DisplayStyleValueType = new StyleValueType(); // value is null or a LayoutManagerConstructor
+ module.exports.DisplayStyleValueType.addParser((tokens) => {
+   let token = tokens.next();
+   if (token.done)
+     throw new Error();
+   if (token.value.kind != 'identifier')
+     throw new Error();
+   if (!displayTypes.has(token.value.value))
+     throw new Error();
+   return {
+     value: displayTypes.get(token.value.value),
+   }
+ });
+
+ internals.registerProperty({
+   name: 'display',
+   type: module.exports.DisplayStyleValueType,
+   inherits: false,
+   initialValue: internals.BlockLayoutManager,
+   needsLayout: true,
+ });
+
+ module.exports.PositiveLengthStyleValueType = new StyleValueType(); // value is a ParsedValue whose value (once resolved) is a number in 96dpi pixels, >=0
+ module.exports.PositiveLengthStyleValueType.addParser((tokens) => {
+   // just handle "<number>px"
+   let token = tokens.next();
+   if (token.done)
+     throw new Error();
+   if (token.value.kind != 'dimension')
+     throw new Error();
+   if (token.value.unit != 'px')
+     throw new Error();
+   if (token.value.value < 0)
+     throw new Error();
+   return {
+     value: token.value.value;
+   };
+ });
+
+ internals.registerProperty({
+   name: 'min-width',
+   type: module.exports.PositiveLengthStyleValueType,
+   inherits: false,
+   initialValue: 0,
+   needsLayout: true,
+ });
+ internals.registerProperty({
+   name: 'min-height',
+   type: module.exports.PositiveLengthStyleValueType,
+   inherits: false,
+   initialValue: 0,
+   needsLayout: true,
+ });
+
+ module.exports.PositiveLengthOrAutoStyleValueType = new StyleValueType(); // value is a ParsedValue whose value (once resolved) is either a number in 96dpi pixels (>=0) or null (meaning 'auto')
+ module.exports.PositiveLengthOrAutoStyleValueType.addParser((tokens) => {
+   // handle 'auto'
+   let token = tokens.next();
+   if (token.done)
+     throw new Error();
+   if (token.value.kind != 'identifier')
+     throw new Error();
+   if (token.value.value != 'auto')
+     throw new Error();
+   return {
+     value: null,
+   };
+ });
+ module.exports.PositiveLengthOrAutoStyleValueType.addParser((tokens) => {
+   return module.exports.PositiveLengthStyleValueType.parse(tokens);
+ });
+
+ internals.registerProperty({
+   name: 'width',
+   type: module.exports.PositiveLengthOrAutoStyleValueType,
+   inherits: false,
+   initialValue: null,
+   needsLayout: true,
+ });
+ internals.registerProperty({
+   name: 'height',
+   type: module.exporets.PositiveLengthOrAutoStyleValueType,
+   inherits: false,
+   initialValue: null,
+   needsLayout: true,
+ });
+
+ module.exports.PositiveLengthOrInfinityStyleValueType = new StyleValueType(); // value is a ParsedValue whose value (once resolved) is either a number in 96dpi pixels (>=0) or Infinity
+ module.exports.PositiveLengthOrInfinityStyleValueType.addParser((tokens) => {
+   // handle 'infinity'
+   let token = tokens.next();
+   if (token.done)
+     throw new Error();
+   if (token.value.kind != 'identifier')
+     throw new Error();
+   if (token.value.value != 'infinity')
+     throw new Error();
+   return {
+     value: Infinity,
+   };
+ });
+ module.exports.PositiveLengthOrInfinityStyleValueType.addParser((tokens) => {
+   return module.exports.PositiveLengthStyleValueType.parse(tokens);
+ });
+
+ internals.registerProperty({
+   name: 'width',
+   type: module.exports.PositiveLengthOrInfinityStyleValueType,
+   inherits: false,
+   initialValue: Infinity,
+   needsLayout: true,
+ });
+ internals.registerProperty({
+   name: 'height',
+   type: module.exporets.PositiveLengthOrInfinityStyleValueType,
+   inherits: false,
+   initialValue: Infinity,
+   needsLayout: true,
+ });
+</script>
diff --git a/sky/examples/style/toolbar-layout.sky b/sky/examples/style/toolbar-layout.sky
new file mode 100644
index 0000000..a43ce3f
--- /dev/null
+++ b/sky/examples/style/toolbar-layout.sky
@@ -0,0 +1,208 @@
+SKY MODULE
+<import src="sky:core" as="sky"/>
+<script>
+ // display: toolbar;
+ // toolbar-spacing: <length>
+ // display: spring; // remaining space is split equally amongst the springs
+ // children are vertically centered, layout out left-to-right with toolbar-spacing space between them
+ // last child is hidden by default unless there's not enough room for the others, then it's shown last, right-aligned
+ module.exports.SpringLayoutManager = class SpringLayoutManager extends sky.LayoutManager { }
+ sky.registerLayoutManager('spring', module.exports.SpringLayoutManager);
+ sky.registerProperty({
+   name: 'toolbar-spacing',
+   type: sky.LengthStyleValueType,
+   inherits: true,
+   initialValue: { value: 8, unit: 'px' },
+   needsLayout: true,
+ });
+ module.exports.ToolbarLayoutManager = class ToolbarLayoutManager extends sky.LayoutManager {
+   constructor (styleNode) {
+     super(styleNode);
+     this.showingOverflow = false;
+     this.firstSkippedChild = null;
+     this.overflowChild = null;
+   }
+   function layout(width, height) {
+     let children = null;
+     let loop = null;
+     if (height == null)
+       height = this.getIntrinsicHeight().value;
+     if (width == null)
+       this.assumeDimensions(0, height);
+     else
+       this.assumeDimensions(width, height);
+     let spacing = this.node.getProperty('toolbar-spacing');
+     if (typeof spacing != 'number')
+       spacing = 0;
+     this.overflowChild = null;
+     this.firstSkippedChild = null;
+
+     // layout children and figure out whether we need to truncate the child list and show the overflow child
+     let springCount = 0;
+     let minX = 0;
+     let overflowChildWidth = 0;
+     let pendingSpacing = 0;
+     children = this.walkChildren();
+     loop = children.next();
+     while (!loop.done) {
+       let child = loop.value;
+       let dims = null;
+       if (child.layoutManager instanceof module.exports.SpringLayoutManager) {
+         springCount += 1;
+         pendingSpacing = spacing; // not +=, because we only have one extra spacing per batch of springs
+       } else {
+         if (child.needsLayout) {
+           childHeight = child.layoutManager.getIntrinsicHeight();
+           if (childHeight.value < height)
+             childHeight = childHeight.value;
+           else
+             childHeight = height;
+           dims = child.layoutManager.layout(width, height);
+           this.setChildSize(child, dims.width, dims.height);
+         } else {
+           dims = {
+             width: child.width,
+             height: child.height,
+           };
+         }
+         loop = children.next();
+         if (!loop.done) {
+           if (minX > 0)
+             minX += spacing + pendingSpacing;
+           minX += dims.width;
+           pendingSpacing = 0;
+         } else {
+           overflowChildWidth = spacing + dims.width;
+           this.overflowChild = child;
+         }
+       }
+     }
+
+     // figure out the spacing
+     this.showingOverflow = false;
+     let springSize = 0;
+     if (width != null) {
+       if (minX <= width) {
+         if (springCount > 0)
+           springSize = (width - minX) / sprintCount;
+       } else {
+         this.showingOverflow = true;
+       }
+     } else {
+       width = minX;
+     }
+
+     // position the children
+     // TODO(ianh): support rtl toolbars
+     let x = 0;
+     let lastWasNonSpring = false;
+     children = this.walkChildren();
+     loop = children.next();
+     while (!loop.done) {
+       let child = loop.value;
+       if (child.layoutManager instanceof module.exports.SpringLayoutManager) {
+         x += springSize;
+         if (lastWasNonSpring)
+           x += spacing;
+         lastWasNonSpring = false;
+       } else {
+         if (!loop.done) {
+           if (x + child.width + overflowChildWidth > width) {
+             this.firstSkippedChild = child;
+             break; // don't display any more children
+           }
+           this.setChildPosition(child, x, (height - child.height)/2);
+           x += child.width + spacing;
+           lastWasNonSpring = true;
+         } else {
+           // assert: this.showingOverflow == false
+         }
+       }
+     }
+     if (this.showingOverflow)
+       this.setChildPosition(this.overflowChild, width-this.overflowChild.width, (height - this.overflowChild.height)/2);
+     else
+       this.firstSkippedChild = this.overflowChild;
+
+     return {
+       width: width,
+       height: height,
+     }
+   }
+   function getIntrinsicWidth() {
+     let width = this.node.getProperty('width');
+     if (typeof height != 'number') {
+       let spacing = this.node.getProperty('toolbar-spacing');
+       if (typeof spacing != 'number')
+         spacing = 0;
+       width = 0;
+       let children = this.walkChildren();
+       let loop = children.next();
+       // we exclude the last child because at our ideal width we wouldn't need it
+       let last1 = null; // last one
+       let last2 = null; // one before the last one
+       while (!loop.done) {
+         if (last1)
+           width += last1.layoutManager.getIntrinsicWidth().value;
+         if (last2)
+           width += spacing;
+         last2 = last1;
+         last1 = loop.value;
+         loop = children.next();
+       }
+     }
+     return super(width); // applies and provides our own min-width/max-width rules
+   }
+   function getIntrinsicHeight() {
+     // we grow our minimum height to be no smaller than the children's
+     let result = super();
+     let determineHeight = false;
+     let heightProperty = this.node.getProperty('height');
+     if (typeof heightProperty != 'number')
+       determineHeight = true;
+     let children = this.walkChildren();
+     let loop = children.next();
+     // here we include the last child so that if it pops in our height doesn't change
+     while (!loop.done) {
+       let child = loop.value;
+       let childHeight = child.layoutManager.getIntrinsicHeight();
+       if (determineHeight) {
+         if (result.value < childHeight.value)
+           result.value = childHeight.value;
+       }
+       if (result.minimum < childHeight.minimum)
+         result.minimum = childHeight.minimum;
+       loop = children.next();
+     }
+     if (result.minimum > result.maximum)
+       result.maximum = result.minimum;
+     if (result.value > result.maximum)
+       result.value = result.maximum;
+     if (result.value < result.minimum)
+       result.value = result.minimum;
+     return result;
+   }
+   function paintChildren(canvas) {
+     let width = this.node.width;
+     let loop = children.next();
+     while ((!loop.done) && (loop.value != this.firstSkippedChild))
+       this.paintChild(loop.value, canvas);
+     if (this.showingOverflow)
+       this.paintChild(this.overflowChild, canvas);
+   }
+   function paintChild(child, canvas) {
+     if (child.needsPaint) {
+       canvas.save();
+       try {
+         canvas.beginPath();
+         canvas.rect(child.x, child.y, child.width, child.height);
+         canvas.clip();
+         child.paint(canvas);
+       } finally {
+         canvas.restore();
+       }
+     }
+   }
+ }
+ sky.registerLayoutManager('toolbar', module.exports.ToolbarLayoutManager);
+</script>
diff --git a/sky/specs/apis.md b/sky/specs/apis.md
index 2346960..b75b5aa 100644
--- a/sky/specs/apis.md
+++ b/sky/specs/apis.md
@@ -151,17 +151,17 @@
   // BUILT-IN ELEMENTS
 
   class ImportElement : Element {
-    constructor (Dictionary attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
+    constructor (Dictionary<String> attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
     constructor (ChildArguments... nodes); // shorthand
-    constructor (Dictionary attributes); // shorthand
+    constructor (Dictionary<String> attributes); // shorthand
     constructor (); // shorthand
     constructor attribute String tagName; // O(1) // "import"
     constructor attribute Boolean shadow; // O(1) // false
   }
   class TemplateElement : Element {
-    constructor (Dictionary attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
+    constructor (Dictionary<String> attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
     constructor (ChildArguments... nodes); // shorthand
-    constructor (Dictionary attributes); // shorthand
+    constructor (Dictionary<String> attributes); // shorthand
     constructor (); // shorthand
     constructor attribute String tagName; // O(1) // "template"
     constructor attribute Boolean shadow; // O(1) // false
@@ -169,25 +169,25 @@
     readonly attribute DocumentFragment content; // O(1)
   }
   class ScriptElement : Element {
-    constructor (Dictionary attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
+    constructor (Dictionary<String> attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
     constructor (ChildArguments... nodes); // shorthand
-    constructor (Dictionary attributes); // shorthand
+    constructor (Dictionary<String> attributes); // shorthand
     constructor (); // shorthand
     constructor attribute String tagName; // O(1) // "script"
     constructor attribute Boolean shadow; // O(1) // false
   }
   class StyleElement : Element {
-    constructor (Dictionary attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
+    constructor (Dictionary<String> attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
     constructor (ChildArguments... nodes); // shorthand
-    constructor (Dictionary attributes); // shorthand
+    constructor (Dictionary<String> attributes); // shorthand
     constructor (); // shorthand
     constructor attribute String tagName; // O(1) // "style"
     constructor attribute Boolean shadow; // O(1) // false
   }
   class ContentElement : Element {
-    constructor (Dictionary attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
+    constructor (Dictionary<String> attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
     constructor (ChildArguments... nodes); // shorthand
-    constructor (Dictionary attributes); // shorthand
+    constructor (Dictionary<String> attributes); // shorthand
     constructor (); // shorthand
     constructor attribute String tagName; // O(1) // "content"
     constructor attribute Boolean shadow; // O(1) // false
@@ -195,65 +195,65 @@
     Array<Node> getDistributedNodes(); // O(N) in distributed nodes
   }
   class ImgElement : Element {
-    constructor (Dictionary attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
+    constructor (Dictionary<String> attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
     constructor (ChildArguments... nodes); // shorthand
-    constructor (Dictionary attributes); // shorthand
+    constructor (Dictionary<String> attributes); // shorthand
     constructor (); // shorthand
     constructor attribute String tagName; // O(1) // "img"
     constructor attribute Boolean shadow; // O(1) // false
   }
   class DivElement : Element {
-    constructor (Dictionary attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
+    constructor (Dictionary<String> attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
     constructor (ChildArguments... nodes); // shorthand
-    constructor (Dictionary attributes); // shorthand
+    constructor (Dictionary<String> attributes); // shorthand
     constructor (); // shorthand
     constructor attribute String tagName; // O(1) // "div"
     constructor attribute Boolean shadow; // O(1) // false
   }
   class SpanElement : Element {
-    constructor (Dictionary attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
+    constructor (Dictionary<String> attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
     constructor (ChildArguments... nodes); // shorthand
-    constructor (Dictionary attributes); // shorthand
+    constructor (Dictionary<String> attributes); // shorthand
     constructor (); // shorthand
     constructor attribute String tagName; // O(1) // "span"
     constructor attribute Boolean shadow; // O(1) // false
   }
   class IframeElement : Element {
-    constructor (Dictionary attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
+    constructor (Dictionary<String> attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
     constructor (ChildArguments... nodes); // shorthand
-    constructor (Dictionary attributes); // shorthand
+    constructor (Dictionary<String> attributes); // shorthand
     constructor (); // shorthand
     constructor attribute String tagName; // O(1) // "iframe"
     constructor attribute Boolean shadow; // O(1) // false
   }
   class TElement : Element {
-    constructor (Dictionary attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
+    constructor (Dictionary<String> attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
     constructor (ChildArguments... nodes); // shorthand
-    constructor (Dictionary attributes); // shorthand
+    constructor (Dictionary<String> attributes); // shorthand
     constructor (); // shorthand
     constructor attribute String tagName; // O(1) // "t"
     constructor attribute Boolean shadow; // O(1) // false
   }
   class AElement : Element {
-    constructor (Dictionary attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
+    constructor (Dictionary<String> attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
     constructor (ChildArguments... nodes); // shorthand
-    constructor (Dictionary attributes); // shorthand
+    constructor (Dictionary<String> attributes); // shorthand
     constructor (); // shorthand
     constructor attribute String tagName; // O(1) // "a"
     constructor attribute Boolean shadow; // O(1) // false
   }
   class TitleElement : Element {
-    constructor (Dictionary attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
+    constructor (Dictionary<String> attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
     constructor (ChildArguments... nodes); // shorthand
-    constructor (Dictionary attributes); // shorthand
+    constructor (Dictionary<String> attributes); // shorthand
     constructor (); // shorthand
     constructor attribute String tagName; // O(1) // "title"
     constructor attribute Boolean shadow; // O(1) // false
   }
   class ErrorElement : Element {
-    constructor (Dictionary attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
+    constructor (Dictionary<String> attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
     constructor (ChildArguments... nodes); // shorthand
-    constructor (Dictionary attributes); // shorthand
+    constructor (Dictionary<String> attributes); // shorthand
     constructor (); // shorthand
     constructor attribute String tagName; // O(1) // "error"
     constructor attribute Boolean shadow; // O(1) // false
@@ -271,9 +271,9 @@
   }
 
   interface ElementConstructor {
-    constructor (Dictionary attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
+    constructor (Dictionary<String> attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
     constructor (ChildArguments... nodes); // shorthand
-    constructor (Dictionary attributes); // shorthand
+    constructor (Dictionary<String> attributes); // shorthand
     constructor (); // shorthand
 
     constructor attribute String tagName;
@@ -452,14 +452,16 @@
 
 * ``Integer`` - WebIDL ``long long``
 * ``Float`` - WebIDL ``double``
+* ``Infinity`` - singleton type with value ``Infinity``
 * ``String`` - WebIDL ``USVString``
 * ``Boolean`` - WebIDL ``boolean``
 # ``Object`` - WebIDL ``object`` (``ClassName`` can be used as a literal for this type)
 * ``ClassName`` - an instance of the class ClassName
 * ``DictionaryName`` - an instance of the dictionary DictionaryName
 * ``Promise<Type>`` - WebIDL ``Promise<T>``
+* ``Generator<Type>`` - An ECMAScript generator function that returns data of the given type
 * ``Array<Type>`` - WebIDL ``sequence<T>``
-* ``Dictionary`` - unordered set of name-value String-String pairs with no duplicate names
+* ``Dictionary<Type>`` - unordered set of name-value String-Type pairs with no duplicate names
 * ``Type?`` - union of Type and the singleton type with value ``null`` (WebIDL nullable)
 * ``(Type1 or Type2)`` - union of Type1 and Type2 (WebIDL union)
 * ``any`` - union of all types (WebIDL ``any``)
diff --git a/sky/specs/style.md b/sky/specs/style.md
index 21c84cd..93c9d4a 100644
--- a/sky/specs/style.md
+++ b/sky/specs/style.md
@@ -1,21 +1,510 @@
 Sky Style Language
 ==================
 
-For now, the Sky style language is CSS with the following restrictions:
-
-- No combinators
-- Only = and ~= attribute selectors
-- Lots of other selectors removed // TODO(ianh): list them
-- Floats removed
-- Lots of other layout models removed // TODO(ianh): list them
-
-
 Planed changes
 --------------
 
 Add //-to-end-of-line comments to be consistent with the script
 language.
 
-Add a way to add new values, e.g. by default only support #RRGGBB
-colours (or maybe only rgba() colours), but provide a way to enable
-CSS4-like "color(red rgb(+ #004400))" stuff.
+
+Style Parser
+------------
+
+(this section is incomplete)
+
+### Tokenisation
+
+
+#### Value parser
+
+
+##### **Value** state
+
+If the current character is...
+
+* '``;``': Consume the character and exit the value parser
+  successfully.
+
+* '``@``': Consume the character and switch to the **at**
+  state.
+
+* '``#``': Consume the character and switch to the **hash**
+  state.
+
+* '``$``': Consume the character and switch to the **dollar**
+  state.
+
+* '``%``': Consume the character and switch to the **percent**
+  state.
+
+* '``&``': Consume the character and switch to the **ampersand**
+  state.
+
+* '``'``': Set _value_ to the empty string, consume the character, and
+  switch to the **single-quoted string** state.
+
+* '``"``': Set _value_ to the empty string, consume the character, and
+  switch to the **double-quoted string** state.
+
+* '``-``': Consume the character, and switch to the **negative
+  integer** state.
+
+* '``0``'-'``9``': Set _value_ to the decimal value of the current
+  character, consume the character, and switch to the **integer**
+  state.
+
+* '``a``'-'``z``', '``A``'-'``Z``': Set _value_ to the current
+  character, consume the character, and switch to the **identifier**
+  state.
+
+* '``*``', '``^``', '``!``', '``?``', '``,``', '``/``', '``<``',
+  '``[``', '``)``', '``>``', '``]``', '``+``': Emit a symbol token
+  with the current character as the symbol, consume the character, and
+  stay in this state.
+
+* Anything else: Consume the character and switch to the **error**
+  state.
+
+
+##### **At** state
+
+* '``0``'-'``9``', '``a``'-'``z``', '``A``'-'``Z``': Set _value_ to
+  the current character, create a literal token with the unit set to
+  ``@``, consume the character, and switch to the **literal** state.
+
+* Anything else: Emit a symbol token with ``@`` as the symbol, and
+  switch to the **value** state without consuming the character.
+
+
+##### **Hash** state
+
+* '``0``'-'``9``', '``a``'-'``z``', '``A``'-'``Z``': Set _value_ to
+  the current character, create a literal token with the unit set to
+  ``@``, consume the character, and switch to the **literal** state.
+
+* Anything else: Emit a symbol token with ``#`` as the symbol, and
+  switch to the **value** state without consuming the character.
+
+
+##### **Dollar** state
+
+* '``0``'-'``9``', '``a``'-'``z``', '``A``'-'``Z``': Set _value_ to
+  the current character, create a literal token with the unit set to
+  ``@``, consume the character, and switch to the **literal** state.
+
+* Anything else: Emit a symbol token with ``$`` as the symbol, and
+  switch to the **value** state without consuming the character.
+
+
+##### **Percent** state
+
+* '``0``'-'``9``', '``a``'-'``z``', '``A``'-'``Z``': Set _value_ to
+  the current character, create a literal token with the unit set to
+  ``@``, consume the character, and switch to the **literal** state.
+
+* Anything else: Emit a symbol token with ``%`` as the symbol, and
+  switch to the **value** state without consuming the character.
+
+
+##### **Ampersand** state
+
+* '``0``'-'``9``', '``a``'-'``z``', '``A``'-'``Z``': Set _value_ to
+  the current character, create a literal token with the unit set to
+  ``@``, consume the character, and switch to the **literal** state.
+
+* Anything else: Emit a symbol token with ``&`` as the symbol, and
+  switch to the **value** state without consuming the character.
+
+
+##### TODO(ianh): more states...
+
+
+##### **Error** state
+
+If the current character is...
+
+* '``;``': Consume the character and exit the value parser in failure.
+
+* Anything else: Consume the character and stay in this state.
+
+
+
+Selectors
+---------
+
+Sky Style uses whatever SelectorQuery. Maybe one day we'll make
+SelectorQuery support being extended to support arbitrary selectors,
+but for now, it supports:
+
+   tagname
+   #id
+   .class
+   [attrname]
+   [attrname=value]
+   :host
+
+These can be combined (without whitespace), with at most one tagname,
+as in:
+
+   tagname[attrname]#id:host.class.class[attrname=value]
+
+In debug mode, giving two IDs, or the same selector twice (e.g. the
+same classname), or specifying other redundant or conflicting
+selectors (e.g. [foo][foo=bar], or [foo=bar][foo=baz]) will be
+flagged.
+
+Alternatively, a selector can be the following special value:
+
+   @document
+
+
+Value Parser
+------------
+
+  class StyleToken {
+    constructor (String king, String value);
+    readonly attribute String kind;
+       // string
+       // identifier
+       // function (identifier + '(')
+       // number
+       // symbol (one of @#$%& if not immediately following numeric or preceding alphanumeric, or one of *^!?,/<[)>]+ or, if not followed by a digit, -)
+       // dimension (number + identifier or number + one of @#$%&)
+       // literal (one of @#$%& + alphanumeric)
+    readonly attribute String value;
+    readonly attribute String unit; // for 'dimension' type, this is the punctuation or identifier that follows the number, for 'literal' type, this is the punctuation that precedes it
+  }
+
+  class TokenSource {
+    constructor (Array<StyleToken> tokens);
+    IteratorResult next();
+    TokenSourceBookmark getBookmark();
+    void rewind(TokenSourceBookmark bookmark);
+  }
+  class TokenSourceBookmark {
+    constructor ();
+    // TokenSource stores unforgeable state on this object using symbols or a weakmap or some such
+  }
+
+  dictionary ParsedValue {
+    any value = null;
+    ValueResolver? resolver = null;
+    Boolean relativeDimension = false; // if true, e.g. for % lengths, the callback will be called again if an ancestor's dimensions change
+    Painter? painter = null;
+  }
+
+  // best practice convention: if you're creating a property with needsPaint, you should 
+  // create a new style value type for it so that it can set the paint callback right;
+  // you should never use such a style type when parsing another property
+
+  callback any ParserCallback (TokenSource tokens);
+
+  class StyleValueType {
+    constructor ();
+    void addParser(ParserCallback parser);
+    any parse(TokenSource tokens, Boolean root = false);
+     // for each parser callback that was registered, in reverse
+     // order (most recently registered first), run these steps:
+     //   let bookmark = tokens.getBookmark();
+     //   try { 
+     //     let result = parser(tokens);
+     //     if (root) {
+     //       if (!tokens.next().done)
+     //         throw new Error();
+     //     }
+     //   } except {
+     //     tokens.rewind(bookmark);
+     //   }
+     // (root is set when you need to parse the entire token stream to be valid)
+  }
+
+  // note: if you define a style value type that uses other style value types, e.g. a "length pair" that accepts two lengths, then
+  // if any of the subtypes have a resolver, you need to make sure you have a resolver that calls them to compute the final value
+
+  dictionary PropertySettings {
+    String name;
+    StyleValueType type; // the output from the parser is coerced to a ParsedValue
+    Boolean inherits = false;
+    any initialValue = null;
+    Boolean needsLayout = false;
+    Boolean needsPaint = false;
+  }
+
+  void registerProperty(PropertySettings propertySettings);
+    // when you register a new property, document the format that is expected to be cascaded
+    // (the output from the propertySettings.type parser's ParsedValue.value field after the resolver, if any, has been called)
+
+  // sky:core exports a bunch of style value types so that people can
+  // extend them
+  attribute StyleValueType PositiveLengthOrInfinityStyleValueType;
+  attribute StyleValueType PositiveLengthOrAutoStyleValueType;
+  attribute StyleValueType PositiveLengthStyleValueType;
+  attribute StyleValueType DisplayStyleValueType;
+  
+
+Inline Styles
+-------------
+
+partial class Element {
+  readonly attribute StyleDeclarationList style;
+}
+
+class StyleDeclarationList {
+  constructor ();
+  void add(StyleDeclaration styles); // O(1) // in debug mode, throws if the dictionary has any properties that aren't registered
+  void remove(StyleDeclaration styles); // O(N) in number of declarations
+  Array<StyleDeclaration> getDeclarations(); // O(N) in number of declarations
+}
+
+typedef StyleDeclaration Dictionary<ParsedValue>;
+
+
+Rule Matching
+-------------
+
+partial class StyleElement {
+  Array<Rule> getRules(); // O(N) in rules
+}
+
+class Rule {
+  constructor ();
+  attribute SelectorQuery selector; // O(1)
+  attribute StyleDeclaration styles; // O(1)
+}
+
+Each frame, at some defined point relative to requestAnimationFrame():
+ - If a rule starts applying to an element, sky:core calls thatElement.style.add(rule.styles);
+ - If a rule stops applying to an element, sky:core calls thatElement.style.remove(rule.styles);
+
+
+Cascade
+-------
+
+For each Element, the StyleDeclarationList is conceptually flattened
+so that only the last declaration mentioning a property is left.
+
+Create the flattened render tree as a tree of StyleNode objects
+(described below). For each one, run the equivalent of the following
+code:
+
+  var display = node.getProperty('display');
+  if (display) {
+    node.layoutManager = new display(node, ownerManager);
+    return true;
+  }
+  return false;
+
+If that code returns false, then that node an all its descendants must
+be dropped from the render tree.
+
+If any node is removed in this pass relative to the previous pass, and
+it has an ownerLayoutManager, then call
+  ``node.ownerLayoutManager.release(node)``
+...to notify the layout manager that the node went away, then set the
+node's layoutManager and ownerLayoutManager attributes to null.
+
+  callback any ValueResolver (any value, String propertyName, StyleNode node, Float containerWidth, Float containerHeight);
+
+  class StyleNode {
+    // this is generated before layout
+    readonly attribute String text;
+    readonly attribute Node? parentNode;
+    readonly attribute Node? firstChild;
+    readonly attribute Node? nextSibling;
+
+    // access to the results of the cascade
+    any getProperty(String name);
+       // if there's a cached value, return it
+       // otherwise, if there's an applicable ParsedValue, then
+       //   if it has a resolver:
+       //     call it
+       //     cache the value
+       //     if relativeDimension is true, then mark the value as provisional
+       //     return the value
+       //   otherwise use the ParsedValue's value; cache it; return it
+       // otherwise, if the property is inherited and there's a parent:
+       //   get it from the parent; cache it; return it
+       // otherwise, get the default value; cache it; return it
+
+    readonly attribute Boolean needsLayout; // means that a property with needsLayout:true has changed on this node or one of its descendants
+    readonly attribute LayoutManager layoutManager;
+
+    readonly attribute LayoutManager ownerLayoutManager; // defaults to the parentNode.layoutManager
+      // if you are not the ownerLayoutManager, then ignore this StyleNode in layout() and paintChildren()
+      // using walkChildren() does this for you
+
+    readonly attribute Boolean needsPaint; // means that either needsLayout is true or a property with needsPaint:true has changed on this node or one of its descendants
+
+    // only the ownerLayoutManager can change these
+    readonly attribute Float x;
+    readonly attribute Float y;
+    readonly attribute Float width;
+    readonly attribute Float height;
+  }
+
+The flattened tree is represented as a hierarchy of Node objects. For
+any element that only contains text node children, the "text" property
+is set accordingly. For elements with mixed text node and non-text
+node children, each run of text nodes is represented as a separate
+Node with the "text" property set accordingly and the styles set as if
+the Node inherited everything inheritable from its parent.
+
+
+Layout
+------
+
+sky:core registers 'display' as follows:
+
+  {
+    name: 'display',
+    type: sky.DisplayStyleValueType,
+    inherits: false,
+    initialValue: sky.BlockLayoutManager,
+    needsLayout: true,
+  }
+
+The following API is then used to add new layout manager types to 'display':
+
+  void registerLayoutManager(String displayValue, LayoutManagerConstructor? layoutManager);
+
+sky:core by default registers:
+
+  'block': sky.BlockLayoutManager
+  'paragraph': sky.ParagraphLayoutManager
+  'inline': sky.InlineLayoutManager
+  'none': null
+
+
+Layout managers inherit from the following API:
+
+  class LayoutManager {
+    readonly attribute StyleNode node;
+    constructor LayoutManager(StyleNode node);
+
+    void take(StyleNode victim); // sets victim.ownerLayoutManager = this;
+      // assert: victim hasn't been take()n yet during this layout
+      // assert: victim.needsLayout == true
+      // assert: an ancestor of victim has needsLayout == this (aka, victim is a descendant of this.node)
+
+    virtual void release(StyleNode victim);
+      // called when the StyleNode was removed from the tree
+
+    void setChildPosition(child, x, y); // sets child.x, child.y
+    void setChildX(child, y); // sets child.x
+    void setChildY(child, y); // sets child.y
+    void setChildSize(child, width, height); // sets child.width, child.height
+    void setChildWidth(child, width); // sets child.width
+    void setChildHeight(child, height); // sets child.height
+      // these set needsPaint on the node and on any node impacted by this (?)
+      // for setChildSize/Width/Height: if the new dimension is different than the last assumed dimensions, and
+      // any StyleNodes with an ownerLayoutManager==this have cached values for getProperty() that are marked
+      // as provisional, clear them
+
+    Generator<StyleNode> walkChildren();
+      // returns a generator that iterates over the children, skipping any whose ownerLayoutManager is not this
+
+    void paint(RenderingSurface canvas);
+      // set a clip rect on the canvas
+      // call the painter of each property, in order they were registered, which on this element has a painter
+      // call this.paintChildren()
+      // unset the clip
+
+    virtual void paintChildren(RenderingSurface canvas);
+      // just calls paint() for each child returned by walkChildren() whose needsPaint is true
+
+    void assumeDimensions(Float width, Float height);
+      // sets the assumed dimensions for calls to getProperty() on StyleNodes that have this as an ownerLayoutManager
+      // if the new dimension is different than the last assumed dimensions, and any StyleNodes with an
+      // ownerLayoutManager==this have cached values for getProperty() that are marked as provisional, clear them
+      // TODO(ianh): should we force this to match the input to layout(), when called from inside layout() and when
+      // layout() has a forced width and/or height?
+
+    virtual LayoutValueRange getIntrinsicWidth(Float? defaultWidth = null);
+      // returns min-width, width, and max-width, normalised, defaulting to values given in LayoutValueRange
+      // if argument is provided, it overrides width
+
+    virtual LayoutValueRange getIntrinsicHeight(Float? defaultHeight = null);
+      // returns min-height, height, and max-height, normalised, defaulting to values given in LayoutValueRange
+      // if argument is provided, it overrides height
+
+    virtual Dimensions layout(Number? width, Number? height);
+      // returns { }
+      // the return value should include the final value for whichever of the width and height arguments that is null
+      // TODO(ianh): should we just grab the width and height from assumeDimensions()?
+
+  }
+
+  dictionary LayoutValueRange {
+    // negative values here should be treated as zero
+    Float minimum = 0;
+    Float value = 0; // ideal desired width; if it's not in the range minimum .. maximum then it overrides minimum and maximum
+    (Float or Infinity) maximum = Infinity; 
+  }
+
+  dictionary Dimensions {
+    Float width = 0;
+    Float height = 0;
+  }
+
+
+Given a tree of StyleNode objects rooted at /node/, the application is
+rendered as follows:
+
+   node.layoutManager.layout(screen.width, screen.height);
+   node.layoutManager.paint();
+
+
+
+Paint
+-----
+
+  callback void Painter (StyleNode node, RenderingSurface canvas);
+
+  class RenderingSurface {
+    // ...
+  }
+
+
+Default Styles
+--------------
+
+In the constructors for the default elements, they add to themselves
+StyleDeclaration objects as follows:
+
+* ``import``
+* ``template``
+* ``style``
+* ``script``
+* ``content``
+* ``title``
+  These all add to themselves the same declaration with value:
+  ``{ display: { value: null } }``
+
+* ``img``
+  This adds to itself the declaration with value:
+  ``{ display: { value: sky.ImageElementLayoutManager } }``
+
+* ``span``
+* ``a``
+  These all add to themselves the same declaration with value:
+  ``{ display: { value: sky.InlineLayoutManager } }``
+
+* ``iframe``
+  This adds to itself the declaration with value:
+  ``{ display: { value: sky.IFrameElementLayoutManager } }``
+
+* ``t``
+  This adds to itself the declaration with value:
+  ``{ display: { value: sky.ParagraphLayoutManager } }``
+
+* ``error``
+  This adds to itself the declaration with value:
+  ``{ display: { value: sky.ErrorLayoutManager } }``
+
+The ``div`` element doesn't have any default styles.
+
+These declarations are all shared between all the elements (so e.g. if
+you reach in and change the declaration that was added to a ``title``
+element, you're going to change the styles of all the other
+default-hidden elements).