In [1]:
from descr import boxy_notebook, descr, HTMLRuleBuilder as RB
pr = boxy_notebook(always_setup = True)
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]:
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)
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:
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, compoundedThe value for these attributes may be:
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)
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
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)
:rearrange
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)
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)
:before
Shortcut: RuleBuilder.before(selector[, function])
Attribute: :after
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)
:htmlreplace
RuleBuilder.htmlreplace(selector[, function])
:join
RuleBuilder.join(selector[, function])
:wrap
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)
In [10]: