Rules

This section covers the various ways to manipulate the display of objects using rules. First let's instantiate our printer:


In [1]:
from descr import boxy_notebook, descr, HTMLRuleBuilder as RB
pr = boxy_notebook(always_setup = True)

What is a rule?

descr works by applying a certain amount of rules on the description. A rule is of the form:

(selector, {attr: value, ...}

They are applied in the order that they are found. The attributes in the dictionary may be CSS attributes (color, border, etc.) or special attributes which are prefixed by : (:rearrange, :hide, :+classes, etc.). The CSS attributes are simply dumped in a stylesheet, whereas the others are processed by Python, and their values may be functions.

Some attributes may be compounded. That is to say, if you define an attribute for the selector .bla multiple times, and that this attribute is marked as compounded, all values will be kept in a list and they will all be applied according to the attribute's semantics. Still, if you set the attribute to None, previous values will be discarded (but only for that precise selector).

In order to facilitate building a list of rules, there is the RuleBuilder class and the HTMLRuleBuilder class, the latter of which offers a few handy shortcuts over the former (but they are otherwise the same). So for instance you can write RuleBuilder().hide(selector) instead of RuleBuilder().rule(selector, {":hide": lambda x, y: True}). The list of rules is stored rather plainly in .rules, so you can always use that as a point of reference to understand what you are building at a lower level:


In [2]:
RB().hide(".blabla").rules


Out[2]:
[('.blabla', {':hide': <function descr.format.<lambda>>})]

CSS

The most straightforward thing you may want to do is manipulate the stylesheet used to style the description. Use HTMLRuleBuilder.css_<attribute>(selector, value). If the attribute contains underscores, they will be automatically changed into dashes. The rules thus constructed will be dumped in a stylesheet in the order that the selectors are defined and the later definitions override the former.


In [3]:
rules = RB()
rules.css_border(".sequence", "2px solid red")
rules.css_border(".sequence .sequence", "2px solid green")
rules.css_border(".sequence .sequence .sequence", "2px solid blue")
rules.css_background_color(".{@int}:hover", "#faa")

from random import randint
pr([[[randint(0, 100)
      for k in range(randint(1, 5))]
     for j in range(randint(1, 5))]
    for i in range(10)],
   rules = rules)


4171082045626962786012595732813513108488115763707063625413884540703624543822332769277976045434944797208255226365335993534763923171931353713738379375519816278896216931044

For the moment, you cannot define Python functions to circumscribe the applicability of the rules, though you can get around that limitation by programmatically adding classes:

Class manipulation

You can programmatically add and remove classes from select elements. Three special attributes control class manipulation:

  • :classes completely redefines the classes for the selection, not compounded (last defined has force of law)
  • :+classes adds classes to the selection, compounded
  • :-classes removes classes from the selection, compounded

The value for these attributes may be:

  • A string, which represents a single class
  • A set of strings, which represents a set of classes
  • A function, which takes a set of classes and a list of children, and returns a class or set of classes

The following methods are offered as shortcuts:

  • RuleBuilder.classes(selector, value) for :classes
  • RuleBuilder.pclasses(selector, value[, function]) for :+classes
  • RuleBuilder.mclasses(selector, value[, function]) for :-classes
  • RuleBuilder.pmclasses(selector, v1, v2) combines :+classes and :-classes

If a value and a function are provided, the class or set of classes listed as value will be added or removed only if the function returns True. The value may also be a function that returns a class or set of classes, as explained above.

Note: these rules are run until equilibrium. It is possible to get stuck in an infinite loop, e.g. if pclasses and mclasses return the same class on intersecting selectors, so don't do anything crazy.


In [11]:
rules = RB().classes(".{@str}", {"@set"})
pr("yours", "truly", rules = rules)
rules = RB().pclasses(".{@str}", {"@set"})
pr("yours", "truly", rules = rules)
rules = RB().mclasses(".{@str}", {"scalar"})
pr("yours", "truly", rules = rules)
rules = RB().pclasses(".{@str}", "hl1", lambda classes, children: children[0] == "truly")
pr("yours", "truly", rules = rules)
# run your cursor over the results (@set:hover has a css rule for blue border)


yourstruly
yourstruly
yourstruly
yourstruly

Replace

  • Attribute: :replace
  • Shortcut: RuleBuilder.replace(selector, function)

The function takes a set of classes and a list of children and returns a new set of classes and a new set of children. It is run right after the class modifiers.


In [5]:
rules = RB().replace(".{@str}", lambda classes, children: ({"@list", "sequence"}, list(children)*3))
pr("bloody mary", rules = rules)
# I hope your screen isn't glossy


bloody marybloody marybloody mary

Hide

  • Attribute: :hide
  • Shortcut: RuleBuilder.hide(selector[, function])

The function takes a set of classes and a list of children and returns True or False. If True then the node is scrapped and not shown. If there is no function, then the selected elements are hidden unconditionally.


In [6]:
rules = RB().hide(".{@tuple} > .{@str}")
pr(("a", ("b", "c"), ["d", "e"], "f"), rules = rules)
rules = RB().hide(".sequence", lambda classes, children: any(child == descr("hide") for child in children))
pr(["a", ("b", "c"), ["d", "hide"], "f"], rules = rules)


de
abcf

Rearrange

  • Attribute: :rearrange
  • Shortcut: RuleBuilder.rearrange(selector[, function])

Again, takes a set of classes and a list of children. Returns a list of new children. That list cannot contain sets. This is run after replace, at which point the classes for this node are fixed and cannot be changed.


In [7]:
rules = RB().rearrange(".sequence", lambda classes, children: list(reversed(children)))
pr([1, 2, 3, [4, [5, 6]], 7, (8, 9)], rules = rules)


987654321

You can emulate replace to some extent by returning a singleton list with a new node in it, but the effect may be different, because the node's classes remain unchanged and thus they will keep being formatted as before. For instance, the replace example using rearrange would look like this:


In [8]:
rules = RB().rearrange(".{@str}", lambda classes, children: [({"@list", "sequence"}, list(children)*3)])
pr("bloody mary", rules = rules)


bloody marybloody marybloody mary

Before and after

  • Attribute: :before
  • Shortcut: RuleBuilder.before(selector[, function])

  • Attribute: :after

  • Shortcut: RuleBuilder.after(selector[, function])

Run after rearrange, they serve to prepend or append elements to the node's existing children. They are cumulative, and the first defined are the first applied.


In [9]:
rules = RB().before(".{@str}", "(((").after(".{@str}", ")))")
pr("bananas", rules = rules)

rules = RB().before(".{@str}", "1").before(".{@str}", "2").before(".{@str}", "3")
rules = rules.after(".{@str}", "1").after(".{@str}", "2").after(".{@str}", "3")
pr("<0>", rules = rules)

rules = RB().before(".{@list}", lambda classes, children: ({"assoc"}, "len", len(children)))
pr([1, 2, ["x"]*10], rules = rules)


(((bananas)))
321<0>123
len312len10xxxxxxxxxx

htmlreplace, join, wrap

  • Attribute: :htmlreplace
  • Shortcut: RuleBuilder.htmlreplace(selector[, function])
  • Attribute: :join
  • Shortcut: RuleBuilder.join(selector[, function])
  • Attribute: :wrap
  • Shortcut: RuleBuilder.wrap(selector[, function])

Whereas the previous methods are run before the children are processed, these methods are run after the children are processed. They all receive a set of classes and a list of children (strings or HTMLNode instances).

htmlreplace must return a set of classes, which will be the final classes in the resulting HTML for this node, and a list of children (must be strings or HTMLNode instances).

join and wrap both take a set of classes and a list of children and must return a new list of children. The difference is just that join is meant to add separators and the like, and wrap to prepend or append new children, but in reality they can do pretty much whatever.


In [10]:
rules = RB().htmlreplace(".{@str}", lambda classes, children: ({"@int"}, children))
pr("hello", rules = rules)

from descr import make_joiner
rules = RB().join(".{@list}", make_joiner("<br/>"))
pr([1, 2, 3], rules = rules)

from descr.html import HTMLNode
rules = RB().wrap(".{@list}", lambda classes, children: [HTMLNode({"@str"}, ["((("])] + children + [HTMLNode({"@str"}, [")))"])])
pr([1, 2, 3], rules = rules)


hello
1
2
3
(((123)))

In [10]: