tabnas-css 0.5.16

Parse CSS into a reworkcss-style abstract syntax tree.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
; CSS's render: a stylesheet's tree written back as CSS text. This is a
; library of definitions, each named `css-...`, which a host links with
; its own program (alchemy's `compile_sources`); its entry point is
; `css-render`, and nothing else here is the host's to call.
;
; The profile writes the reader's own tree and no other, as the
; translation design says a render of a format whose value has a shape of
; its own does: the manifest names that shape `css-ast`, the reworkcss/css
; model the reader builds. Every node is an object with a `type` and the
; members its type gives it:
;
;   stylesheet    rules (the root, and only the root)
;   rule          selectors, declarations
;   declaration   property, value
;   comment       comment
;   keyframes     name, vendor (when there is one), keyframes
;   keyframe      values, declarations
;   page          selectors, declarations
;   document      document, vendor, rules
;   custom-media  name, media
;   host          rules
;
; and every other at-rule is named by its `type`: a block whose body is
; rules holds a member named after the type, the prelude, and `rules`
; (`media`, `supports`, `layer`, ...); a block whose body is declarations
; holds `declarations` alone (`font-face`, `viewport`, `counter-style`,
; ...); a statement holds the member named after the type alone, its
; params (`import`, `charset`, `namespace`, ...). A member whose name no
; node type gives a meaning is taken as an at-rule's own, its prelude,
; since alchemy compares a name only with a written one, and a node holds
; at most one. A node may also hold `position`, which the reader adds when
; asked to: an object, `start` and `end`, read and not written, since a
; reader derives it from the text again. A `position` that holds a string
; is no source position but the prelude of an at-rule named `position`,
; as `@position x {}` reads, and is written as one.
;
; The members may come in any order. The reader builds them in the order
; above, `type` first, and the TypeScript and Rust trees keep it; the Go
; port's nodes are plain maps, which a Go host walks in sorted key order,
; where `type` comes after the members whose text it decides. So a node
; writes nothing until what it writes is decided, and its text is the
; same whatever order its members came in.
;
; Selectors, values, preludes, params, properties and comments are the
; raw text the reader keeps (quotes, escapes and comments inside a
; prelude as written), so each is written as it is, and the render checks
; neither its characters nor how CSS reads an at-rule's keyword: a tree
; the reader could not have built (a comment holding `*/`, a selector
; holding a top-level comma, declarations under a keyword CSS reads as
; rules) is written as its members say and does not read back as itself.
; Any other member, a member a node's type does not give it, a root that
; is not a stylesheet, a declaration outside a block of declarations, a
; keyframe outside a block of keyframes, a rule with no selector and a
; keyframe with no value fail with TARGET_VALUE_UNREPRESENTABLE, naming
; what was met.
;
; The text: the stylesheet's nodes one after another, a blank line
; between two, and one line feed after the last; an empty stylesheet is
; no text at all. A block's nodes are one per line, two spaces deeper than
; the line that opens it, and its closing brace is on a line of its own;
; an empty block is `{}` on its opening line. A rule is its selectors, one
; per line, a comma after each but the last, then its block; a declaration
; is `property: value;`; a comment is `/*` its text `*/`; a keyframe is its
; values joined by `, `, then its block. An at-rule is `@` and its keyword,
; then its prelude, and a block or `;`: `@media screen {`, `@page :first
; {`, `@-webkit-keyframes name {`, `@-moz-document url-prefix() {`,
; `@import url(a.css);`, `@custom-media --small (max-width: 30em);`. Two
; nodes have no keyword of their own, since the reader names them by the
; params of a `@type` at-rule: a node of a type alone is written
; `@type name;`, and one of a type with rules, other than `host`,
; `@type name {` and its rules. Every statement ends with `;`.
;
; The state is two vectors: the frames, which grow with the stylesheet's
; nesting and never with its width, and the blocks held back, innermost
; last. A node is `[:node indentation kind type members block]`: the kind
; of list it is in, its type once it has come, its other members so far as
; `[slot value]` pairs, a slot for each name some node type gives a meaning
; and one for a name none does (`css-slot-of`), and its block, `:none`
; until one begins. What a node writes is decided by its type, its list
; and its members together (`css-compose`), and a block's text comes after
; its opening. So when a block begins, the opening is written and the
; block follows it as it comes (`:streamed`) if the members so far decide
; the opening and no member the node may still hold would change it;
; otherwise the block is held back (`:held`) until the node ends, which
; decides the opening, and is written then, after it. The reader's order
; decides every opening before its block but three, which a later member
; could still change and so are held: an unprefixed `@keyframes` (a
; vendor), `@host` and a node of a type alone with rules (a prelude). A
; block held back is a rope of four strings, each taking in the next's
; text when that passes a size, so it stays four strings however long it
; grows and a character is copied a bounded number of times; its text
; counts toward the state's bound, `max_metadata_bytes`, which bounds what
; can be held. Text goes to the output, or to the innermost block held
; back when there is one.
;
; A list (the stylesheet's rules, a block's rules, declarations or
; keyframes) is `[:list kind indentation parent's-indentation written]`,
; where written says whether a node came before, which decides the
; separator. A node's selectors or a keyframe's values are `[:strs name
; indentation one-per-line one-line written]`, written both ways a list of
; selectors is, each a rope, until the node's type says which. A member's
; value due is `[:member indentation kind type members block name]`, and
; the root's `[:root-member type rules name]`; the root is `[:root type
; rules]`, whether each has come. A source position being passed over is
; `[:skip opens]`, one marker in opens per container open inside it. Once
; the stylesheet ends the frames are `[:done]`, and nothing may follow it.
; Events no tree has fail with PROTOCOL_ORDER_ERROR, saying so.
;
; Each node of the stylesheet's own list (a rule, an at-rule, a comment)
; is a record of the text: the step that ends one answers alchemy's
; `:record-end` after its outputs, and so does the step that opens the
; list, so a host's writer holds a node until its end, and a run that
; fails part way leaves whole nodes behind.

; The top frame, or :none before the root.
def css-top [s]
  match (count s)
    case 0 :none
    case _ (top s)

; The top frame replaced.
def css-mark [frame s]
  push frame (pop s)

; Two words with a space between, or the first alone when the second is
; empty: an at-keyword and its prelude, say.
def css-words [a b]
  match b
    case "" a
    case _ (string-join " " [a b])

; An at-keyword.
def css-at [keyword]
  string-join "" ["@" keyword]

def css-comment [text]
  string-join "" ["/*" text "*/"]

; What comes before a list's node: nothing before the stylesheet's first,
; a blank line between two of its nodes, and in a block a line break and
; the block's indentation.
def css-lead [lk ind written]
  match [lk written]
    case [:sheet false] ""
    case [:sheet true] "\n\n"
    case _ (string-join "" ["\n" ind])

; What ends a list: a line feed after the stylesheet's last node, and a
; block's closing brace, on its own line when the block holds a node.
def css-close [lk pind written]
  match [lk written]
    case [:sheet false] ""
    case [:sheet true] "\n"
    case [_ false] "}"
    case _ (string-join "" ["\n" pind "}"])

; A text held back: four strings, the newest text last. A piece joins the
; last, and a string that passes its size joins the one before it, so a
; character is copied a few times however long the text grows, and the
; rope stays four strings.
def css-rope-empty ["" "" "" ""]

def css-rope-add [r x]
  match r
    case [a b c d] (css-rope-d a b c (string-join "" [d x]))

def css-rope-d [a b c d]
  match (compare (length d) 256)
    case :less [a b c d]
    case _ (css-rope-c a b (string-join "" [c d]))

def css-rope-c [a b c]
  match (compare (length c) 8192)
    case :less [a b c ""]
    case _ (css-rope-b a (string-join "" [b c]))

def css-rope-b [a b]
  match (compare (length b) 262144)
    case :less [a b "" ""]
    case _ [(string-join "" [a b]) "" "" ""]

def css-rope-text [r]
  string-join "" r

; The slot of a member named for a meaning some node type gives it, which
; a node of the type named the same holds as its own, its prelude; null
; for any other name.
def css-own-slot [name]
  match name
    case "property" 6
    case "value" 7
    case "selectors" 1
    case "declarations" 3
    case "rules" 4
    case "values" 2
    case "keyframes" 5
    case "name" 8
    case "vendor" 9
    case "media" 10
    case "comment" 11
    case "supports" 12
    case "document" 13
    case "position" 14
    case _ null

; The slot a member is held in: its own, or the last.
def css-slot-of [name]
  match (css-own-slot name)
    case null 15
    case i i

; A node's members so far, `[slot value]` each, in the order they came:
; a node holds a few, each once.
def css-slot [i f]
  match (filter (partial css-in-slot i) f)
    case [] null
    case [[k v]] v
    case _ :twice

def css-in-slot [i p]
  match p
    case [k v] (css-same i k)

def css-same [i k]
  match (compare i k)
    case :equal true
    case _ false

; What a member's slot holds: its value, and the name of one whose name no
; node type gives a meaning.
def css-held [i name value]
  match i
    case 15 [name value]
    case _ value

; A block a slot holds, by its kind, or null.
def css-block-in [x]
  match x
    case [:body lk] lk
    case _ null

def css-out-of-place-text "a node's members are not the ones its type has"

def css-not-keyframe-text "a block of keyframes holds a node that is not a keyframe or a comment"

def css-no-declarations-text "a rule, a page or a keyframe has no declarations"

def css-prelude-text "an at-rule's prelude is not a string"

def css-selectors-text "a rule's or a page's selectors are not an array"

def css-bad [message]
  fail :unrepresentable (string-join "" ["the tree is not a CSS stylesheet: " message])

; What a node writes, decided by its type, the kind of list it is in and
; its members, whatever order they came in: `[:block opening pending]`,
; what opens its block, and whether a member it may still hold would
; change that; `[:leaf text]`, all of a node with no block; or
; `[:bad message]`, for a node CSS has no form for. The first case is a
; declaration, the commonest node, whose members are held in that order
; whether its type comes first or last.
def css-compose [ctx t f]
  match [t ctx f]
    case ["declaration" :decls [[6 property] [7 value]]] [:leaf (string-join "" [property ":" (css-words "" value) ";"])]
    case [null _ _] [:bad "a node has no type"]
    case [_ :kfs _] (css-in-keyframes t f)
    case _ (css-in-block ctx t f)

; A node in a block of keyframes: a keyframe or a comment.
def css-in-keyframes [t f]
  match t
    case "keyframe" (css-keyframe f)
    case "comment" (css-kf-comment f)
    case _ [:bad css-not-keyframe-text]

def css-keyframe [f]
  match [(css-slot 2 f) (css-slot 3 f) (count f)]
    case [[:strs nl cs true] [:body :decls] 2] [:block cs false]
    case [[:strs nl cs false] [:body :decls] 2] [:bad "a keyframe has no value, which CSS has no form for"]
    case [[:strs nl cs any] null 1] [:bad css-no-declarations-text]
    case _ [:bad css-not-keyframe-text]

def css-kf-comment [f]
  match [(css-slot 11 f) (count f)]
    case [null _] [:bad css-not-keyframe-text]
    case [c 1] [:leaf (css-comment c)]
    case _ [:bad css-not-keyframe-text]

; A node in the stylesheet or a block of rules or declarations: a type
; whose members are its own when it holds the member that marks them, and
; otherwise an at-rule.
def css-in-block [ctx t f]
  match t
    case "declaration" (css-declaration-or ctx t f)
    case "rule" (css-rule-or ctx t f)
    case "page" (css-page-or ctx t f)
    case "keyframes" (css-keyframes-or ctx t f)
    case "custom-media" (css-custom-media-or ctx t f)
    case "keyframe" (css-keyframe-outside ctx t f)
    case _ (css-at-rule ctx t f)

def css-rule-or [ctx t f]
  match (css-slot 1 f)
    case [:strs nl cs any] (css-rule nl any f)
    case null (css-at-rule ctx t f)
    case _ [:bad css-selectors-text]

def css-rule [nl any f]
  match [any (css-slot 3 f) (count f)]
    case [true [:body :decls] 2] [:block nl false]
    case [false [:body :decls] 2] [:bad "a rule has no selector, which CSS has no form for"]
    case [_ null 1] [:bad css-no-declarations-text]
    case _ [:bad css-out-of-place-text]

def css-page-or [ctx t f]
  match (css-slot 1 f)
    case [:strs nl cs any] (css-page cs any f)
    case null (css-at-rule ctx t f)
    case _ [:bad css-selectors-text]

def css-page [cs any f]
  match [any (css-slot 3 f) (count f)]
    case [true [:body :decls] 2] [:block (string-join " " ["@page" cs]) false]
    case [false [:body :decls] 2] [:block "@page" false]
    case [_ null 1] [:bad css-no-declarations-text]
    case _ [:bad css-out-of-place-text]

def css-declaration-or [ctx t f]
  match (css-slot 6 f)
    case null (css-at-rule ctx t f)
    case property (css-declaration ctx property f)

def css-declaration [ctx property f]
  match [ctx (css-slot 7 f) (count f)]
    case [:decls null 1] [:bad "a declaration has no value"]
    case [:decls value 2] [:leaf (string-join "" [property ":" (css-words "" value) ";"])]
    case [:decls _ _] [:bad css-out-of-place-text]
    case _ [:bad "a declaration is outside a block of declarations"]

def css-keyframes-or [ctx t f]
  match (css-slot 8 f)
    case null (css-at-rule ctx t f)
    case name (css-keyframes name f)

def css-keyframes [name f]
  match [(css-slot 5 f) (css-slot 9 f) (count f)]
    case [[:body :kfs] null 2] [:block (css-words "@keyframes" name) true]
    case [[:body :kfs] vendor 3] [:block (css-words (string-join "" ["@" vendor "keyframes"]) name) false]
    case [null null 1] [:bad "a keyframes has no keyframes"]
    case [null vendor 2] [:bad "a keyframes has no keyframes"]
    case _ [:bad css-out-of-place-text]

def css-custom-media-or [ctx t f]
  match (css-slot 8 f)
    case null (css-at-rule ctx t f)
    case name (css-custom-media name f)

def css-custom-media [name f]
  match [(css-slot 10 f) (count f)]
    case [null 1] [:bad "a custom media query has no media"]
    case [media 2] [:leaf (string-join "" [(css-words (css-words "@custom-media" name) media) ";"])]
    case _ [:bad css-out-of-place-text]

def css-keyframe-outside [ctx t f]
  match (css-slot 2 f)
    case null (css-at-rule ctx t f)
    case _ [:bad "a keyframe is outside a block of keyframes"]

; An at-rule: its prelude is the member named after its type, when that
; name has a meaning, or the member whose name has none, and it holds at
; most one; `rules` named after the type is the block of `@rules`.
def css-at-rule [ctx t f]
  css-at-rule-own ctx t (css-own-value t f) (css-slot 15 f) f

def css-own-value [t f]
  match (css-own-slot t)
    case null null
    case i (css-slot i f)

def css-at-rule-own [ctx t own other f]
  match [own other]
    case [null null] (css-no-prelude t f)
    case [null [n p]] (css-prelude t p f)
    case [[:body :rules] null] (css-no-prelude t f)
    case [[:body :rules] [n p]] (css-prelude t p f)
    case [[:body lk] _] [:bad css-prelude-text]
    case [[:strs nl cs any] _] [:bad css-prelude-text]
    case [:twice _] [:bad css-out-of-place-text]
    case [p null] (css-prelude t p f)
    case _ [:bad css-out-of-place-text]

; An at-rule with no prelude: a type alone, or a block.
def css-no-prelude [t f]
  match [(css-block-in (css-slot 4 f)) (css-block-in (css-slot 3 f))]
    case [null null] (css-lone t f)
    case [:rules null] (css-rules-only t f)
    case [null :decls] (css-decls-only t f)
    case _ [:bad css-out-of-place-text]

def css-lone [t f]
  match (count f)
    case 0 [:leaf (string-join "" [(css-words "@type" t) ";"])]
    case _ [:bad css-out-of-place-text]

; A block of rules with no prelude, which a later prelude would change.
def css-rules-only [t f]
  match (count f)
    case 1 [:block (css-rules-opening t) true]
    case _ [:bad css-out-of-place-text]

def css-rules-opening [t]
  match t
    case "host" "@host"
    case "rules" "@rules"
    case _ (css-words "@type" t)

def css-decls-only [t f]
  match [t (count f)]
    case ["rule" _] [:bad css-out-of-place-text]
    case ["page" _] [:bad css-out-of-place-text]
    case ["keyframe" _] [:bad css-out-of-place-text]
    case [_ 1] [:block (css-at t) false]
    case _ [:bad css-out-of-place-text]

; An at-rule with its prelude p: a statement, or a block of rules, and a
; document's vendor before its keyword.
def css-prelude [t p f]
  match [(css-block-in (css-slot 4 f)) (css-block-in (css-slot 3 f)) t]
    case [:rules null "document"] (css-document p f)
    case [:rules null _] (css-prelude-block t p f)
    case [null null "document"] (css-document-statement p f)
    case [null null _] (css-statement t p f)
    case _ [:bad css-out-of-place-text]

def css-prelude-block [t p f]
  match (count f)
    case 2 [:block (css-words (css-at t) p) false]
    case _ [:bad css-out-of-place-text]

def css-statement [t p f]
  match [t (count f)]
    case ["comment" 1] [:leaf (css-comment p)]
    case [_ 1] [:leaf (string-join "" [(css-words (css-at t) p) ";"])]
    case _ [:bad css-out-of-place-text]

; A document's block, its vendor, which a later vendor would change when
; it has none, before its keyword.
def css-document [p f]
  match [(css-slot 9 f) (count f)]
    case [null 2] [:block (css-words "@document" p) true]
    case [vendor 3] [:block (css-words (string-join "" ["@" vendor "document"]) p) false]
    case _ [:bad css-out-of-place-text]

def css-document-statement [p f]
  match [(css-slot 9 f) (count f)]
    case [null _] (css-statement "document" p f)
    case [_ 2] [:bad "a document has no rules"]
    case _ [:bad css-out-of-place-text]

; What a member's value is not, named by the member.
def css-not-string [name]
  match name
    case "type" "a node's type is not a string"
    case "selectors" css-selectors-text
    case "values" "a keyframe's values are not an array"
    case "declarations" "a node's declarations are not an array"
    case "rules" "a node's rules are not an array"
    case "keyframes" "a keyframes' keyframes are not an array"
    case "property" "a declaration's property is not a string"
    case "value" "a declaration's value is not a string"
    case "name" "a keyframes' or a custom media query's name is not a string"
    case "vendor" "a vendor prefix is not a string"
    case "media" "a media query list is not a string"
    case "comment" "a comment's text is not a string"
    case "position" "a node's position is neither a source position, an object, nor an at-rule's prelude, a string"
    case _ css-prelude-text

; A value met where the tree's shape has no place for it.
def css-misplaced [s]
  match (css-top s)
    case :none (css-bad "its root is not a node")
    case [:root-member ty ru "type"] (css-bad "its root's type is not stylesheet")
    case [:root-member ty ru "rules"] (css-bad "its rules are not an array")
    case [:root-member ty ru name] (css-bad "its root's position is not a source position, an object")
    case [:list lk ind pind written] (css-bad "a list of rules, declarations or keyframes holds a value that is not a node")
    case [:strs "values" ind nl cs any] (css-bad "a keyframe's value is not a string")
    case [:strs name ind nl cs any] (css-bad "a selector is not a string")
    case [:member ind ctx t f body name] (css-bad (css-not-string name))
    case _ (fail :protocol-order "the events hold a value where a key is due, which a tree's never do")

; The outputs of a step that ends a node of the list at the top of s,
; with the end of a record after them when the list is the stylesheet's
; own and nothing is held back: the node is written whole.
def css-ended [s held outputs]
  match [(css-top s) held]
    case [[:list :sheet ind pind written] []] (push :record-end outputs)
    case _ outputs

; Text that ends a node of the list at the top of s, written as
; css-write writes it, with the end of a record after it where css-ended
; says.
def css-write-ended [s held text]
  match held
    case [] (transition [s held] (css-ended s held [text]))
    case _ (css-write s held text)

; Text written: to the output, or to the innermost block held back.
def css-write [s held text]
  match [text held]
    case ["" _] (transition [s held] [])
    case [_ []] (transition [s held] [text])
    case _ (transition [s (css-mark (css-rope-add (top held) text) held)] [])

; A source position, an object, passed over from just inside it.
def css-skip [opens event s held]
  match event
    case object-start (transition [(css-mark [:skip (push :open opens)] s) held] [])
    case array-start (transition [(css-mark [:skip (push :open opens)] s) held] [])
    case object-end (css-skip-end opens s held)
    case array-end (css-skip-end opens s held)
    case _ (transition [s held] [])

def css-skip-end [opens s held]
  match (count opens)
    case 1 (transition [(pop s) held] [])
    case _ (transition [(css-mark [:skip (pop opens)] s) held] [])

; The root's members: its type, its rules and its position, in any order.
def css-root-key [name ty ru s held]
  match [name ty ru]
    case ["type" false _] (transition [(css-mark [:root-member ty ru name] s) held] [])
    case ["rules" _ false] (transition [(css-mark [:root-member ty ru name] s) held] [])
    case ["position" _ _] (transition [(css-mark [:root-member ty ru name] s) held] [])
    case ["type" true _] (css-bad "its root holds two types")
    case ["rules" _ true] (css-bad "its root holds two lists of rules")
    case _ (css-bad "its root holds a member other than type, rules and position")

; A node's member, and after a block written as it came only its source
; position. A member a node holds twice is found when the node is
; composed.
def css-node-key [name ind ctx t f body s held]
  match [body name]
    case [:streamed "position"] (transition [(css-mark [:member ind ctx t f body name] s) held] [])
    case [:streamed _] (css-bad "a node holds a member after the ones its type has")
    case _ (transition [(css-mark [:member ind ctx t f body name] s) held] [])

def css-key [name s held]
  match (css-top s)
    case [:node ind ctx t f body] (css-node-key name ind ctx t f body s held)
    case [:root ty ru] (css-root-key name ty ru s held)
    case _ (fail :protocol-order "the events hold a key where a value is due, which a tree's never do")

; A member's string: the type, or another member's held in its slot.
def css-member-string [name value ind ctx t f body s held]
  match [(kind value) body name t]
    case [:string :streamed _ _] (css-bad "a node holds a member after the ones its type has")
    case [:string _ "type" null] (transition [(css-mark [:node ind ctx value f body] s) held] [])
    case [:string _ "type" _] (css-bad "a node holds two types")
    case [:string _ _ _] (css-keep (css-slot-of name) name value ind ctx t f body s held)
    case _ (css-misplaced s)

def css-keep [i name value ind ctx t f body s held]
  transition [(css-mark [:node ind ctx t (push [i (css-held i name value)] f) body] s) held] []

; A selector, or a keyframe's value, written both ways a list of them is:
; one per line, a comma after each but the last, and on one line, joined
; by `, `.
def css-strs-string [name value ind nl cs any s held]
  match [(kind value) any]
    case [:string false] (transition [(css-mark [:strs name ind (css-rope-add nl value) (css-rope-add cs value) true] s) held] [])
    case [:string true]
      transition [(css-mark [:strs name ind (css-rope-add nl (string-join "" [",\n" ind value])) (css-rope-add cs (string-join "" [", " value])) true] s) held] []
    case _ (css-misplaced s)

def css-scalar [value s held]
  match (css-top s)
    case [:member ind ctx t f body name] (css-member-string name value ind ctx t f body s held)
    case [:strs name ind nl cs any] (css-strs-string name value ind nl cs any s held)
    case [:root-member false ru "type"]
      match value
        case "stylesheet" (transition [(css-mark [:root true ru] s) held] [])
        case _ (css-misplaced s)
    case _ (css-misplaced s)

; An object: a node of a list, a source position, or the root stylesheet.
def css-object [s held]
  match (css-top s)
    case [:list lk ind pind written]
      css-write (push [:node ind lk null [] :none] (css-mark [:list lk ind pind true] s)) held (css-lead lk ind written)
    case [:member ind ctx t f body "position"] (transition [(push [:skip [:open]] (css-mark [:node ind ctx t f body] s)) held] [])
    case :none (transition [[[:root false false]] held] [])
    case [:root-member ty ru "position"] (transition [(push [:skip [:open]] (css-mark [:root ty ru] s)) held] [])
    case _ (css-misplaced s)

; A block begins: written after its opening when the members so far
; decide it for good, and held back otherwise.
def css-block [lk i ind ctx t f body s held]
  match body
    case :none (css-block-start lk t (push [i [:body lk]] f) ind ctx s held)
    case _ (css-bad "a node holds two blocks")

def css-block-start [lk t f ind ctx s held]
  match (css-compose ctx t f)
    case [:block opening false]
      css-write (push [:list lk (string-join "" [ind "  "]) ind false] (css-mark [:node ind ctx t f :streamed] s)) held (string-join "" [opening " {"])
    case _
      transition [(push [:list lk (string-join "" [ind "  "]) ind false] (css-mark [:node ind ctx t f :held] s)) (push css-rope-empty held)] []

; An array: a node's block, a rule's or a page's selectors, a keyframe's
; values, or the stylesheet's rules.
def css-member-array [name ind ctx t f body s held]
  match name
    case "rules" (css-block :rules 4 ind ctx t f body s held)
    case "declarations" (css-block :decls 3 ind ctx t f body s held)
    case "keyframes" (css-block :kfs 5 ind ctx t f body s held)
    case "selectors" (transition [(push [:strs name ind css-rope-empty css-rope-empty false] (css-mark [:node ind ctx t f body] s)) held] [])
    case "values" (transition [(push [:strs name ind css-rope-empty css-rope-empty false] (css-mark [:node ind ctx t f body] s)) held] [])
    case _ (css-misplaced s)

def css-array [s held]
  match (css-top s)
    case [:member ind ctx t f body name] (css-member-array name ind ctx t f body s held)
    case [:root-member ty false "rules"] (transition [(push [:list :sheet "" "" false] (css-mark [:root ty true] s)) held] [:record-end])
    case _ (css-misplaced s)

; A node ends: its block was written as it came, or is written now after
; its opening, or the node is written whole.
def css-node-end [ctx t f body s held]
  match body
    case :streamed (transition [(pop s) held] (css-ended (pop s) held []))
    case _ (css-node-written body (css-compose ctx t f) (pop s) held)

def css-node-written [body c s held]
  match [body c]
    case [:none [:leaf text]] (css-write-ended s held text)
    case [:held [:block opening pending]] (css-write-ended s (pop held) (string-join "" [opening " {" (css-rope-text (top held))]))
    case [_ [:bad message]] (css-bad message)
    case _ (css-bad css-out-of-place-text)

def css-object-end [s held]
  match (css-top s)
    case [:node ind ctx t f body] (css-node-end ctx t f body s held)
    case [:root true true] (transition [[:done] held] [])
    case [:root false ru] (css-bad "its root has no type")
    case [:root ty false] (css-bad "its root has no rules")
    case :none (fail :protocol-order "the events end an object that is not open, which a tree's never do")
    case [:list lk ind pind written] (fail :protocol-order "the events end an object inside an array, which a tree's never do")
    case [:strs name ind nl cs any] (fail :protocol-order "the events end an object inside an array, which a tree's never do")
    case _ (fail :protocol-order "the events end an object after a key and before its value, which a tree's never do")

; A node's selectors or values end, held in its slot both ways.
def css-strs-end [name nl cs any s held]
  match (css-top s)
    case [:node ind ctx t f body]
      transition [(css-mark [:node ind ctx t (push [(css-slot-of name) [:strs (css-rope-text nl) (css-rope-text cs) any]] f) body] s) held] []

def css-array-end [s held]
  match (css-top s)
    case [:list lk ind pind written] (css-write (pop s) held (css-close lk pind written))
    case [:strs name ind nl cs any] (css-strs-end name nl cs any (pop s) held)
    case _ (fail :protocol-order "the events end an array that is not open, which a tree's never do")

def css-event [s held event]
  match event
    case object-start (css-object s held)
    case array-start (css-array s held)
    case (key name) (css-key name s held)
    case (scalar value) (css-scalar value s held)
    case object-end (css-object-end s held)
    case array-end (css-array-end s held)

def css-step [state event]
  match state
    case [s held]
      match (css-top s)
        case :done (fail :protocol-order "the events hold more after the stylesheet, which a tree's never do")
        case [:skip opens] (css-skip opens event s held)
        case _ (css-event s held event)

def css-finish [state]
  match state
    case [s held]
      match (css-top s)
        case :done []
        case :none (fail :protocol-order "the events hold no value, where a tree's hold one")
        case _ (fail :protocol-order "the events ended inside a container, which a tree's never do")

; The render: the stylesheet's events in, its text out.
def css-render [input]
  join ""
    scan-emit [[] []] css-step css-finish (events input)