replace styled components

This commit is contained in:
tristan 2024-01-01 06:33:57 +00:00
parent 003100e691
commit 30ffa6c9b3
11 changed files with 388 additions and 444 deletions

View file

@ -8,36 +8,52 @@
nixite = import ./nixite/. { inherit pkgs; };
in nixite // {
packages.${system} = {
raw = nixite.mkSite (nixite.site.applyStyle ./testing/src/style.css
(nixite.site.extractPaths {
raw = nixite.mkSite (let
site = (nixite.site.extractPaths {
"test" = with nixite.elems;
(Doc [
let
blue = nixite.style.tag "span" "blue" {
style = { color = "blue"; };
};
underblue = nixite.style.extend blue "under" {
style = { text-decoration = "underline"; };
};
in (Doc { } [
[
(title "Nixite")
(nixite.html.tag "link" {
(title { } "Nixite")
(link {
rel = "shortcut icon";
type = "image/png";
href = ./testing/src/favicon.png;
} "")
(a "/readme" "Readme")
]
(main [
(main { } [
(a "/readme" "Readme")
(a "/blog" "blog")
(List [ "item 1" "item 2" "item 3" ])
(List { } [ "item 1" "item 2" "item 3" ])
(p { } [
"check out my"
(blue "blue span")
"isn't it"
(underblue "great!")
])
])
]);
blog = nixite.md.readDir ./testing/blog;
"index.html" = nixite.md.mdToPage ./README.md;
}));
});
styles = # nixite.site.getStyles site;
nixite.style.getStyles site.test;
in nixite.site.applyStyle styles site);
default = nixite.serve self.packages.${system}.raw;
test = let run = import ./testing/run.nix pkgs;
in run [
#./testing/md.test.nix
./testing/md.test.nix
./testing/html.test.nix
#./testing/elems.test.nix
#./testing/site.test.nix
./testing/elems.test.nix
./testing/site.test.nix
./testing/style.test.nix
];
};

View file

@ -1,60 +1,143 @@
let
html = import ./html.nix;
s = import ./style.nix;
base = tag: (s.styled tag tag { class = [ ]; });
in (base "html") (base "head") (base "title") (base "base") (base "link")
(base "meta")
# (base "style" ) # borks styled coponents
(base "body") (base "article") (base "section") (base "nav") (base "aside")
(base "h1") (base "h2") (base "h3") (base "h4") (base "h5") (base "h6")
(base "hgroup") (base "header") (base "footer") (base "address") (base "p")
(base "hr") (base "pre") (base "blockquote") (base "ol") (base "ul")
(base "menu") (base "li") (base "dl") (base "dt") (base "dd") (base "figure")
(base "figcaption") (base "main") (base "div") (base "a") (base "em")
(base "strong") (base "small") (base "s") (base "cite") (base "q") (base "dfn")
(base "abbr") (base "ruby") (base "rt") (base "rp") (base "data") (base "time")
(base "code") (base "var") (base "samp") (base "kbd") (base "sub") (base "sup")
(base "i") (base "b") (base "u") (base "mark") (base "bdi") (base "bdo")
(base "span") (base "br") (base "wbr") (base "ins") (base "del")
(base "picture") (base "source") (base "img") (base "iframe") (base "embed")
(base "object") (base "param") (base "video") (base "audio") (base "track")
(base "map") (base "area") (base "table") (base "caption") (base "colgroup")
(base "col") (base "tbody") (base "thead") (base "tfoot") (base "tr")
(base "td") (base "th") (base "form") (base "label") (base "input")
(base "button") (base "select") (base "datalist") (base "optgroup")
(base "option") (base "textarea") (base "output") (base "progress")
(base "meter") (base "fieldset") (base "legend") (base "details")
(base "summary") (base "dialog") (base "script") (base "noscript")
(base "template") (base "slot") (base "canvas")
(s.styled "List" "ul" {
__child = child:
let html = import ./html.nix;
in {
html = html.tag "html";
# Document metadata
head = html.tag "head";
title = html.tag "title";
base = html.tag "base";
link = html.tag "link";
meta = html.tag "meta";
style = html.tag "style";
# Sections
body = html.tag "body";
article = html.tag "article";
section = html.tag "section";
nav = html.tag "nav";
aside = html.tag "aside";
h1 = html.tag "h1";
h2 = html.tag "h2";
h3 = html.tag "h3";
h4 = html.tag "h4";
h5 = html.tag "h5";
h6 = html.tag "h6";
hgroup = html.tag "hgroup";
header = html.tag "header";
footer = html.tag "footer";
address = html.tag "address";
# Grouping content
p = html.tag "p";
hr = html.tag "hr";
pre = html.tag "pre";
blockquote = html.tag "blockquote";
ol = html.tag "ol";
ul = html.tag "ul";
menu = html.tag "menu";
li = html.tag "li";
dl = html.tag "dl";
dt = html.tag "dt";
dd = html.tag "dd";
figure = html.tag "figure";
figcaption = html.tag "figcaption";
main = html.tag "main";
div = html.tag "div";
# Text-level semantics
a = params:
html.tag "a"
(if builtins.isString params then { href = params; } else params);
em = html.tag "em";
strong = html.tag "strong";
small = html.tag "small";
s = html.tag "s";
cite = html.tag "cite";
q = html.tag "q";
dfn = html.tag "dfn";
abbr = html.tag "abbr";
ruby = html.tag "ruby";
rt = html.tag "rt";
rp = html.tag "rp";
data = html.tag "data";
time = html.tag "time";
code = html.tag "code";
var = html.tag "var";
samp = html.tag "samp";
kbd = html.tag "kbd";
sub = html.tag "sub";
sup = html.tag "sup";
i = html.tag "i";
b = html.tag "b";
u = html.tag "u";
mark = html.tag "mark";
bdi = html.tag "bdi";
bdo = html.tag "bdo";
span = html.tag "span";
br = html.tag "br";
wbr = html.tag "wbr";
# Edits
ins = html.tag "ins";
del = html.tag "del";
# Embedded content
picture = html.tag "picture";
source = html.tag "source";
img = html.tag "img";
iframe = html.tag "iframe";
embed = html.tag "embed";
object = html.tag "object";
param = html.tag "param";
video = html.tag "video";
audio = html.tag "audio";
track = html.tag "track";
map = html.tag "map";
area = html.tag "area";
# Tabular data
table = html.tag "table";
caption = html.tag "caption";
colgroup = html.tag "colgroup";
col = html.tag "col";
tbody = html.tag "tbody";
thead = html.tag "thead";
tfoot = html.tag "tfoot";
tr = html.tag "tr";
td = html.tag "td";
th = html.tag "th";
# Forms
form = html.tag "form";
label = html.tag "label";
input = html.tag "input";
button = html.tag "button";
select = html.tag "select";
datalist = html.tag "datalist";
optgroup = html.tag "optgroup";
option = html.tag "option";
textarea = html.tag "textarea";
output = html.tag "output";
progress = html.tag "progress";
meter = html.tag "meter";
fieldset = html.tag "fieldset";
legend = html.tag "legend";
# Interactive elements
details = html.tag "details";
summary = html.tag "summary";
dialog = html.tag "dialog";
# Scripting
script = html.tag "script";
noscript = html.tag "noscript";
template = html.tag "template";
slot = html.tag "slot";
canvas = html.tag "canvas";
# custom
List = params: child:
assert builtins.isList child;
(map (html.tag "li" { }) child);
}) (s.styled "Doc" "html" {
__child = child:
html.tag "ul" { } (map (html.tag "li" { }) child);
Doc = params: child:
assert builtins.isList child;
assert builtins.length child == 2;
assert builtins.isList (builtins.elemAt child 0); [
assert builtins.isList (builtins.elemAt child 0);
html.tag "html" ({ lang = "en"; } // params) [
(html.tag "head" { } (builtins.elemAt child 0))
(html.tag "body" { } (builtins.elemAt child 1))
];
lang = "en";
class = [ ];
}) (s.styled "a" "a" {
__self = self: attrs: child:
if builtins.isString attrs then
self { href = attrs; } child
else
self attrs child;
}) (s.styled "Stylesheet" "link" {
Stylesheet = params:
html.tag "link" ({
rel = "stylesheet";
class = [ ];
__self = self: attrs:
if builtins.isString attrs then
self { href = attrs; } ""
else
self attrs "";
}) // {
H = v: child: html.tag "h${toString v}" { } child;
} // (if builtins.isString params then { href = params; } else params)) "";
}

View file

@ -2,8 +2,8 @@ let
keyvalue = key: value:
assert builtins.isString key;
if builtins.isAttrs value then
builtins.trace "Skipping ${key} as it is a set" "" else
if value == "" || value == [ ] then
builtins.trace "Skipping ${key} as it is a set" ""
else if value == "" || value == [ ] then
""
else
''${key}="${toString value}"'';
@ -37,7 +37,9 @@ in rec {
inherit tag attrs;
__functor = self: child:
if !(isTag child) then
throw "tag child must be tag, list, or string"
throw "tag child must be tag, list, or string, got ${
builtins.typeOf child
}"
else {
inherit tag attrs child;
__toString = toHTML;

View file

@ -1,4 +1,15 @@
let elems = import ./elems.nix;
let
elems = import ./elems.nix;
html = import ./html.nix;
H = n:
let
v = if n < 1 then
builtins.trace "attempted to make heading size ${n} (min is 1)" 1
else if n > 6 then
builtins.trace "attempted to make heading size ${n} (max is 6)" 6
else
n;
in html.tag "h${toString v}" { };
in rec {
readMd = md:
if builtins.isPath md then
@ -30,13 +41,14 @@ in rec {
readDir = root: recFixAppendix (recReadMd root);
mdToPage = md: elems.Doc [ [ (elems.title "markdown file") ] (readMd md) ];
mdToPage = md:
elems.Doc { } [ [ (elems.title { } "markdown file") ] (readMd md) ];
mdBlock = block:
let
m = heading block;
h = if m == null then 0 else builtins.stringLength (builtins.elemAt m 0);
in if m == null then elems.p { } block else elems.H h (builtins.elemAt m 1);
in if m == null then elems.p { } block else H h (builtins.elemAt m 1);
heading = block: builtins.match "(#+) (.*)" block;
}

View file

@ -4,6 +4,12 @@ let
in rec {
applyStyle = style: site: (linkStyle site) // { "style.css" = style; };
getStyles = site: ''
.blue {
color: blue;
}
'';
linkStyle = site:
(builtins.mapAttrs (name: content:
if builtins.isAttrs content && content ? "__toString" then

View file

@ -1,16 +1,6 @@
let
html = import ./html.nix;
join = {
__functor = self: new: self // new // { style = self.style // new.style; };
};
mkStyle = identifier: styles: {
${identifier} = styles;
__toString = self:
toString (builtins.attrValues (builtins.mapAttrs styleToString self));
};
styleToString = identifier: styles:
if !builtins.isAttrs styles then
""
@ -26,76 +16,50 @@ let
}
'';
mkIdentifier = name: tag:
assert builtins.isString name;
let
elem = if builtins.isString tag then tag else tag.tag or "";
inheritClass =
if builtins.isString tag then [ ] else tag.attrs.class or [ ];
in { class ? [ name ] ++ inheritClass, id ? "", ... }:
"${elem}" + builtins.concatStringsSep "" (map (c: "." + c) class)
+ (if id != "" then "#" else "") + id;
getStyle = element: if builtins.isAttrs element && element.attrs ? __id
then {${element.attrs.__id} = element.attrs.style or {};} else {};
getStyles = element: ( getStyle element ) //
(if builtins.isList element then getStylesFromList element else
if element ? child then getStyles element.child else {});
getStylesFromList = elements: builtins.zipAttrsWith (name: value: builtins.elemAt value 0) (map getStyles elements);
in {
inherit getStyle getStyles;
stylesToString = styles: builtins.concatStringsSep "" ( builtins.attrValues ( builtins.mapAttrs (styleToString) styles ) );
tag = tag: class: props: (html.tag tag (props // {__id = "${tag}.${class}";}));
styled = name: tag: cprops:
assert builtins.isString name;
let
__child = cprops.__child or (child: child);
joinProps = (cprops // {
__functor = prev: next:
prev // next // {
class = prev.class ++ next.class or [ ];
};
class = if cprops ? class then
if cprops.class == [ ] then [ ] else [ name ] ++ cprops.class
getStyle = element:
if builtins.isAttrs element && element.attrs ? __id then
({
${element.attrs.__id} = element.attrs.style or { };
} // (if element.attrs ? __extends then {
${element.attrs.__extends.attrs.__id} =
element.attrs.__extends.attrs.style or { };
} else
{ }))
else
[ name ];
});
{ };
self = if tag ? __functor && builtins.isFunction tag.__functor then
props: tag (joinProps props)
else if builtins.isString tag then
props: html.tag tag (joinProps props)
getStyles = element:
(getStyle element) // (if builtins.isList element then
getStylesFromList element
else if element ? child then
getStyles element.child
else
throw "You may only style a tag (string) or element, got ${
builtins.typeOf tag
}";
__self = (cprops.__self or (self: props:
if builtins.isAttrs props then
(child: (self props (__child child)))
else if builtins.isString props || builtins.isList props then
(self { } (__child props))
else
throw "Call element with attributes and child.")) self;
in {
${name} = {
tag = (if builtins.isString tag then tag else tag.tag);
attrs =
joinProps (if builtins.isString tag then { } else tag.attrs or { });
__functor = self: __self;
{ }) // {
__toString = stylesToString;
};
style = mkStyle (mkIdentifier name tag cprops) (cprops.style or { });
} // join;
getStylesFromList = elements:
builtins.zipAttrsWith (name: value: builtins.elemAt value 0)
(map getStyles elements);
style = identifier: styles: { style = mkStyle identifier styles; } // join;
mkProps = props: tag: class:
props // {
__id = "${tag}.${builtins.concatStringsSep "." class}";
class = class ++ (props.class or [ ]);
};
stylesToString = styles:
builtins.concatStringsSep ""
(builtins.attrValues (builtins.mapAttrs (styleToString) styles));
in {
inherit getStyle getStyles stylesToString;
extend = tag: class: props:
(html.tag tag.tag ((mkProps props tag.tag (tag.attrs.class ++ [ class ]))
// {
__extends = tag;
}));
tag = tag: class: props: (html.tag tag (mkProps props tag [ class ]));
}

View file

@ -28,26 +28,13 @@ in with elems; [
actual = main { } [ "yeet" ];
asString = true;
})
(it "makes an h1 tag" {
expected = html.tag "h1" { } "foobar";
actual = H 1 "foobar";
asString = true;
})
(it "makes an h2 tag" {
expected = html.tag "h2" { } "foobar";
actual = H 2 "foobar";
asString = true;
})
(it "makes a title tag" {
expected = html.tag "title" { } "foobar";
actual = title { } "foobar";
asString = true;
})
(it "makes an a tag" {
expected = html.tag "a" {
class = [ "a" ];
href = "https://example.com";
} "example";
expected = html.tag "a" { href = "https://example.com"; } "example";
actual = a "https://example.com" "example";
asString = true;
})
@ -60,10 +47,7 @@ in with elems; [
asString = true;
})
(it "makes a list" {
expected = html.tag "ul" {
__ = "";
class = [ "List" ];
} [
expected = html.tag "ul" { __ = ""; } [
(html.tag "li" { } "foo")
(html.tag "li" { } "bar")
(html.tag "li" { } "baz")

View file

@ -31,15 +31,13 @@ in with html; [
};
})
(it "makes element"
(let para = (tag "p" {});
(it "makes element" (let para = (tag "p" { });
in {
expected = "p";
actual = para.tag;
}))
(it "keeps attrs on element"
(let
(it "keeps attrs on element" (let
attrs = { style = { foo = "bar"; }; };
para = (tag "p" attrs);
in {
@ -47,8 +45,7 @@ in with html; [
actual = para.attrs;
}))
(it "makes renderable element"
(let
(it "makes renderable element" (let
attrs = { style = { foo = "bar"; }; };
para = (tag "p" attrs);
in {
@ -56,8 +53,7 @@ in with html; [
actual = toString (para "");
}))
(it "keeps tag"
(let
(it "keeps tag" (let
attrs = { style = { foo = "bar"; }; };
para = (tag "p" attrs);
in {
@ -65,8 +61,7 @@ in with html; [
actual = (para "").tag;
}))
(it "keeps style"
(let
(it "keeps style" (let
attrs = { style = { foo = "bar"; }; };
para = (tag "p" attrs);
in {
@ -74,8 +69,7 @@ in with html; [
actual = (para "").attrs.style;
}))
(it "works recursively"
(let
(it "works recursively" (let
attrs = { style = { foo = "bar"; }; };
para = (tag "p" attrs);
a = (tag "a" { });

View file

@ -1,9 +1,11 @@
msg:
{ actual, expected, asString ? false, asJSON ? false, }:
{ actual, expected, asString ? false, asJSON ? false, removeDunders ? false, }:
if (if asString then
toString actual == toString expected
else if asJSON then
builtins.toJSON actual == builtins.toJSON expected
else if removeDunders then
builtins.removeAttrs actual [ "__toString" "__functor" ] == expected
else
actual == expected) then ''
echo 'it ${msg}'

View file

@ -19,7 +19,7 @@ in with md; [
lorem ipsum
'';
expected = [
(elems.H 1 "foo bar")
(elems.h1 { } "foo bar")
""
(elems.p { } ''
lorem ipsum
@ -33,6 +33,14 @@ in with md; [
expected = "index.html";
})
(it "converts markdown to a page" {
actual = mdToPage ./blog/index.md;
expected = ''
<html lang="en"><head ><title >markdown file</title></head> <body ><h1 >yeee</h1> <p >ye</p> <p >&amp;</p> <p ><a href="dir">dir</a>
<a href="/">home</a></p> <p ></p></body></html>'';
asString = true;
})
(it "recursively reads dir" {
actual = recReadMd ./blog;
expected = {

View file

@ -1,185 +1,16 @@
let
style = import ../nixite/style.nix;
html = import ../nixite/html.nix;
it = import ./it.nix;
in [
(it "makes a p component" (let
my = (style.styled "para" "p" { style = { some-style = "some value"; }; });
in {
expected = html.tag "p" { class = [ "para" ]; } "yes";
actual = my.para { } "yes";
asString = true;
}))
(it "extends existing components" (let
my = (style.styled "generic" "p" {
foo = "bar";
forgetme = "nothing";
});
this = (style.styled "quote" my.generic {
baz = "baz";
forgetme = "forgotten";
});
in {
expected = html.tag "p" {
forgetme = "forgotten";
baz = "baz";
foo = "bar";
class = [ "generic" "quote" ];
} "yes";
actual = this.quote { } "yes";
asString = true;
}))
(it "makes a component with no class"
(let my = (style.styled "classless" "div" { class = [ ]; });
in {
expected = html.tag "div" { class = [ ]; } "yes";
actual = my.classless { } "yes";
asString = true;
}))
(it "does not error without attrs" (let
my = (style.styled "div" "div" {
class = [ "something" ];
style = { this = "that"; };
});
in{
expected = html.tag "div" { class = [ "div" "something" ]; } "yes";
actual = my.div "yes";
asString = true;
}))
(it "makes a component" (let
my = (style.styled "div" "div" {
class = [ "something" ];
style = { this = "that"; };
});
in{
expected = html.tag "div" { class = [ "div" "something" ]; } "foobar";
actual = my.div { } "foobar";
asString = true;
}))
(it "makes special components" (let
my = (style.styled "s" "div" {
id = "s";
class = [ "something" ];
style = { s = "yes"; };
});
in{
expected = html.tag "div" {
id = "s";
class = [ "s" "something" ];
} "foobar";
actual = my.s { } "foobar";
asString = true;
}))
(it "works on many classes" (let
my = (style.styled "foobar" "div" {
class = [ "foo" "bar" ];
style = { something = "something"; };
});
in{
expected = html.tag "div" { class = [ "foobar" "foo" "bar" ]; } "foobar";
actual = my.foobar { } "foobar";
asString = true;
}))
(it "does custom behavour" (let
my = (style.styled "list" "ul" {
__child = child:
assert builtins.isList child;
(map (html.tag "li" { }) child);
});
in {
expected = html.tag "ul" {
__ = "";
class = [ "list" ];
} [ (html.tag "li" { } "1") (html.tag "li" { } "2") ];
actual = my.list { } [ "1" "2" ];
asString = true;
}))
(it "combines attrs" (let
my = (style.styled "div" "div" {
class = [ "something" ];
style = { this = "that"; };
}) (style.styled "s" "div" {
id = "s";
class = [ "something" ];
style = { s = "yes"; };
}) (style.styled "foobar" "div" {
class = [ "foo" "bar" ];
style = { something = "something"; };
}) (style.style "body" { foo = "bar"; }) (style.styled "list" "ul" {
__child = child:
assert builtins.isList child;
(map (html.tag "li" { }) child);
});
in {
expected = html.tag "div" {
id = "foo";
class = [ "div" "something" ];
} "foobar";
actual = my.div { id = "foo"; } "foobar";
asString = true;
}))
(it "makes a style"
(let my = (style.styled "para" "p" { style = { foo = "bar"; }; });
in {
expected = { "p.para" = { foo = "bar"; }; };
actual = removeAttrs my.style [ "__toString" ];
}))
(it "retains tag"
(let p = (style.styled "para" "p" { style = { foo = "bar"; }; });
in {
expected = "p";
actual = p.para.tag;
}))
(it "retains attrs"
(let p = (style.styled "para" "p" { style = { foo = "bar"; }; });
in {
expected = { foo = "bar"; };
actual = p.para.attrs.style;
}))
(it "retains class" (let p = (style.styled "para" "p" { });
in {
expected = [ "para" ];
actual = p.para.attrs.class;
}))
(it "merges styles" (let
p = (style.styled "para" "p" { style = { foo = "bar"; }; });
d = (style.styled "para2" p.para { style = { baz = "bar"; }; });
my = p d;
in {
expected = {
"p.para" = { foo = "bar"; };
"p.para2.para" = {
foo = "bar";
baz = "bar";
};
};
actual = removeAttrs my.style [ "__toString" ];
}))
(it "fetches empty style"
(let
para = (style.tag "p" "para" {});
(it "fetches empty style" (let para = (style.tag "p" "para" { });
in {
expected = { "p.para" = { }; };
actual = style.getStyle (para "");
}))
(it "fetches style"
(let
(it "fetches style" (let
attrs = { style = { foo = "bar"; }; };
para = (style.tag "p" "para" attrs);
in {
@ -187,8 +18,26 @@ in [
actual = style.getStyle (para "");
}))
(it "fetches style for class"
(let
(it "appliess class" (let
attrs = { style = { foo = "bar"; }; };
para = (style.tag "p" "para" attrs);
in {
expected = [ "para" ];
actual = (para "").attrs.class;
}))
(it "applies classes from props" (let
attrs = {
style = { foo = "bar"; };
class = [ "other" "class" ];
};
para = (style.tag "p" "para" attrs);
in {
expected = [ "para" "other" "class" ];
actual = (para "").attrs.class;
}))
(it "fetches style for class" (let
s = { foo = "bar"; };
para = (style.tag "p" "para" { style = s; });
in {
@ -196,8 +45,7 @@ in [
actual = style.getStyle (para "");
}))
(it "fetches style recursively"
(let
(it "fetches style recursively" (let
s = {
"p.para" = { foo = "bar"; };
"a.link" = { this = "that"; };
@ -207,10 +55,10 @@ in [
in {
expected = s;
actual = style.getStyles (para (a "hello"));
removeDunders = true;
}))
(it "fetches style recursively through lists"
(let
(it "fetches style recursively through lists" (let
s = {
"p.para" = { foo = "bar"; };
"a.link" = { this = "that"; };
@ -220,10 +68,10 @@ in [
in {
expected = s;
actual = style.getStyles (para [ (a "hello") ]);
removeDunders = true;
}))
(it "fetches style recursively with repeats"
(let
(it "fetches style recursively with repeats" (let
s = {
"p.para" = { foo = "bar"; };
"a.link" = { this = "that"; };
@ -233,10 +81,10 @@ in [
in {
expected = s;
actual = style.getStyles (para [ (a "hello") (a "hello") ]);
removeDunders = true;
}))
(it "converts styles to string"
(let
(it "converts styles to string" (let
s = {
"p.para" = { foo = "bar"; };
"a.link" = { this = "that"; };
@ -253,4 +101,29 @@ in [
actual = style.stylesToString s;
}))
(it "extends styled tags" (let
s = {
"p.para" = { foo = "bar"; };
"p.para.oof" = { oof = "yes"; };
};
para = (style.tag "p" "para" { style = s."p.para"; });
para2 = (style.extend para "oof" { style = s."p.para.oof"; });
in {
expected = s;
actual = style.getStyles (para2 "");
removeDunders = true;
}))
(it "extends styled tags classes" (let
s = {
"p.para" = { foo = "bar"; };
"p.para.oof" = { oof = "yes"; };
};
para = (style.tag "p" "para" { style = s."p.para"; });
para2 = (style.extend para "oof" { style = s."p.para.oof"; });
in {
expected = [ "para" "oof" ];
actual = (para2 "").attrs.class;
}))
]