Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_sbj/src/validate.rs

66.6 KiB, 1 run

created by r1870400018:22228, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! Schema validation and the limits that are not the decoder's. See `SPEC.md` §4, §5.
2//!
3//! The decoder has already enforced the depth limit and the tree region size before anything gets
4//! here, so what remains is the node schema, the node count, and the rules that only make sense
5//! once a tree exists: the root is a `doc`, every kind code is one the schema admits, every field is
6//! present and correctly typed, and every child is one its parent admits.
7//!
8//! The schema is a vocabulary and not a constant. A document (`oxeweb/doc/0`) admits the kinds 1 to
9//! 13; the browser's chrome admits those and the `edit` node; an application's tree admits those and
10//! the `surface` node as well. One walk validates all three, and which kinds it admits is
11//! [`Schema::admits`] and nothing else, so "a document is never a program" is a fact about this
12//! function rather than a promise made elsewhere.
13//!
14//! The style vocabulary is the schema's in exactly the same way ([`Schema::admits_style`]): a
15//! document admits the eight properties of §4.4, and a chrome and an application admit more,
16//! because the chrome is a real interface and needs to look like one. The browser's own trees are
17//! held to this walk like any document, so the chrome is legal because it conforms, and not because
18//! nobody checked it.
19//!
20//! Nodes are numbered by a depth-first, pre-order walk from 0 at the root (`SPEC.md` §4.6), and
21//! every rejection names the node, its kind, and the rule it broke (`SPEC.md` §6).
22
23use crate::{
24 kinds::{
25 check_address,
26 check_border,
27 icon_names_label,
28 known_icon,
29 known_style_field,
30 Content,
31 Field,
32 FieldType,
33 NodeKind,
34 ReservedKind,
35 Schema,
36 StyleCheck,
37 StyleField,
38 ALIGNMENTS,
39 DIRECTIONS,
40 PALETTE,
41 KEY_ALT,
42 KEY_CHILDREN,
43 KEY_FALLBACK,
44 KEY_STYLE,
45 KEY_STYLES,
46 },
47 limit,
48};
49
50use oxedyne_fe2o3_core::prelude::*;
51use oxedyne_fe2o3_jdat::prelude::*;
52
53/// What a validated tree turned out to contain.
54#[derive(Clone, Copy, Debug, Default)]
55pub struct Stats {
56 /// Number of nodes.
57 pub nodes: usize,
58 /// Greatest nesting depth reached. The root alone is a depth of 1.
59 pub depth: usize,
60}
61
62/// Validates a decoded tree against the vocabulary its schema admits, naming the failing node.
63///
64/// The walk is iterative rather than recursive, so a tree that arrived by some route other than the
65/// decoder, and therefore never met the decoder's depth limit, cannot exhaust the stack here. It
66/// walks the tree by reference for the same reason: cloning a node clones every node under it, and
67/// the clone a derived implementation makes recurses as deep as the tree goes, which would spend
68/// the stack this walk was written to save.
69pub fn validate(tree: &Dat, schema: &str) -> Outcome<Stats> {
70
71 let schema = res!(Schema::from_name(schema));
72
73 let mut stats = Stats::default();
74 let mut id: usize = 0;
75
76 // How many surfaces the tree has opened, against the ceiling of §5. A surface is a live
77 // application instance, and a tree that may open unboundedly many is a tree that exhausts the host
78 // by being opened.
79 let mut surfaces: usize = 0;
80
81 // The names the document's style table defines, filled in when the root doc is reached (§4.4). A
82 // node's `style` field must name one of these, and since the root is popped before any of its
83 // descendants, the table is known before any style reference is checked.
84 let mut styles: Vec<String> = Vec::new();
85
86 // The names some node actually references. A style defined but never used renders identically to
87 // one that was never defined, so an unreferenced entry would give one document two addresses; it
88 // is caught after the walk, once every reference is known.
89 let mut referenced: Vec<String> = Vec::new();
90
91 // A pending node: the daticle, the kind of its parent, its depth, and whether it sits inside a
92 // surface's alternative. Children are pushed in reverse, so popping yields the depth-first
93 // pre-order that numbers the nodes.
94 //
95 // The `inert` flag is what makes an alternative inert. It is set on the nodes of an alternative
96 // and carried down to everything beneath them, so no schema, and no fallback, can put an `edit` or
97 // a `surface` inside the content that stands in for an application. An alternative that could
98 // itself hold a surface would be a hole with another hole behind it.
99 let mut stack: Vec<(&Dat, Option<NodeKind>, usize, bool)> = vec![(tree, None, 1, false)];
100
101 while let Some((node, parent, depth, inert)) = stack.pop() {
102
103 if stats.nodes == limit::NODES {
104 return Err(err!(
105 "Node {}: the tree exceeds the limit of {} nodes.", id, limit::NODES;
106 Invalid, Input, TooBig, LimitReached));
107 }
108 stats.nodes += 1;
109 if depth > stats.depth {
110 stats.depth = depth;
111 }
112
113 // A node is a usr daticle: a kind code, then a payload.
114 let (uid, payload_opt) = match node {
115 Dat::Usr(uid, payload_opt) => (uid, payload_opt),
116 d => return Err(err!(
117 "Node {} is a {} daticle; every node is a usr daticle carrying a kind code.",
118 id, d.kind();
119 Invalid, Input)),
120 };
121
122 let kind = match NodeKind::from_code(uid.code()) {
123 Ok(kind) => kind,
124 Err(_) => {
125 // SPEC §4.2 and §4.5: a code the reader KNOWS, and the schema does not ADMIT, is
126 // refused unconditionally. It is checked before the fallback rule below and never
127 // falls through to it, because the fallback rule is forward compatibility for a code
128 // this version has never heard of, and this is not that: the reader knows exactly what
129 // it has been handed. Admitting a reserved kind on a fallback would let an author put
130 // a surface in a document today, under a fallback that renders innocently, and have
131 // every reader that later learned what code 15 meant begin honouring it -- a document
132 // that became a program by waiting. Anyone who later "simplifies" these two paths back
133 // into one reopens that.
134 if let Some(reserved) = ReservedKind::from_code(uid.code()) {
135 res!(check_reserved(
136 id, reserved, schema, parent, payload_opt, depth, inert,
137 &mut surfaces, &styles, &mut referenced, &mut stack));
138 id += 1;
139 continue;
140 }
141 // SPEC §4.5: a kind outside the vocabulary is permitted only when its payload is a
142 // map carrying a non-empty `fallback` of known nodes, which stand in for it. The
143 // root is never one, since it must be a doc.
144 if id == 0 {
145 return Err(err!(
146 "Node 0 declares the unknown kind code {}; the root of an '{}' payload \
147 must be a doc.", uid.code(), schema.name();
148 Invalid, Input));
149 }
150 res!(push_fallback(id, uid.code(), parent, payload_opt, depth, inert, &mut stack));
151 id += 1;
152 continue;
153 },
154 };
155
156 // The root of a payload is always a doc, whichever schema it declares. A chrome and an
157 // application are documents that may say more, not trees of another shape.
158 if id == 0 && kind != NodeKind::Doc {
159 return Err(err!(
160 "Node 0 is a {}; the root of an '{}' payload must be a doc.",
161 kind.label(), schema.name();
162 Invalid, Input));
163 }
164
165 // A child appears only where its parent admits it.
166 if let Some(pkind) = parent {
167 if !pkind.allows(&kind) {
168 return Err(err!(
169 "Node {} is a {} inside a {}, which admits {} content only.",
170 id, kind.label(), pkind.label(), content_label(pkind.content());
171 Invalid, Input));
172 }
173 }
174
175 let payload = match payload_opt {
176 Some(payload) => payload.as_ref(),
177 None => return Err(err!(
178 "Node {} ({}) carries no payload; a {} carries {}.",
179 id, kind.label(), kind.label(), payload_label(kind);
180 Invalid, Input, Missing)),
181 };
182
183 // Every payload is a map, except a text run's, which is the string itself.
184 if kind.payload_is_str() {
185 match payload {
186 Dat::Str(_) => (),
187 d => return Err(err!(
188 "Node {} ({}) carries a {} payload; a {} carries a str.",
189 id, kind.label(), d.kind(), kind.label();
190 Invalid, Input)),
191 }
192 } else {
193 match payload {
194 Dat::Map(map) => {
195 res!(check_fields(
196 id, kind.label(), kind.fields(), kind.content(), kind == NodeKind::Doc, map));
197 // A heading's level is the one field with a range as well as a width.
198 if kind == NodeKind::Heading {
199 res!(check_heading_level(id, map));
200 }
201 // The root doc's style table is validated once, and its names collected so
202 // that every later `style` reference can be resolved against them (§4.4). The
203 // schema goes with it: which properties a record may name is the schema's
204 // business, exactly as which kinds the tree may carry is.
205 if kind == NodeKind::Doc {
206 styles = res!(check_styles_table(id, schema, map));
207 }
208 // A node naming a style must name one the table defines (§4.4).
209 if let Some(sv) = map.get(&dat!(KEY_STYLE)) {
210 res!(check_style_ref(id, kind.label(), sv, &styles));
211 if let Dat::Str(s) = sv {
212 referenced.push(s.clone());
213 }
214 }
215 },
216 d => return Err(err!(
217 "Node {} ({}) carries a {} payload; a {} carries a map.",
218 id, kind.label(), d.kind(), kind.label();
219 Invalid, Input)),
220 }
221 }
222
223 let kids = match children(payload) {
224 Ok(kids) => kids,
225 Err(e) => return Err(err!(e,
226 "Node {} ({}): its children must be a list.", id, kind.label();
227 Invalid, Input)),
228 };
229 // SPEC §4.2: a list is marked `item+` and carries at least one child. A doc and a section are
230 // `flow*` and may be empty, since a stub with only a title is a legitimate thing to publish.
231 if kind.requires_child() && kids.is_empty() {
232 return Err(err!(
233 "Node {} ({}) carries no children, but a {} must carry at least one.",
234 id, kind.label(), kind.label();
235 Invalid, Input, Missing));
236 }
237 for kid in kids.iter().rev() {
238 stack.push((kid, Some(kind), depth + 1, inert));
239 }
240
241 id += 1;
242 }
243
244 // Every defined style must be used. An unreferenced entry has no rendering effect, so a document
245 // carrying it and one without it are the same document at two addresses (§4.4).
246 for name in &styles {
247 if !referenced.contains(name) {
248 return Err(err!(
249 "Node 0 (doc): style '{}' is defined in the table but no node references it, which \
250 would give one document two addresses; remove it (SPEC.md §4.4).", name;
251 Invalid, Input));
252 }
253 }
254
255 Ok(stats)
256}
257
258/// The children a node payload carries, borrowed rather than cloned.
259///
260/// This is [`children_of`](crate::kinds::children_of) without the clone, which matters because a
261/// validator walks every node of a tree that arrived from somewhere else.
262fn children(payload: &Dat) -> Outcome<&[Dat]> {
263 match payload {
264 Dat::Map(map) => match map.get(&dat!(KEY_CHILDREN)) {
265 None => Ok(&[]),
266 Some(Dat::List(v)) => Ok(&v[..]),
267 Some(d) => Err(err!(
268 "Node children must be a list, found {:?}.", d.kind();
269 Invalid, Input)),
270 },
271 _ => Ok(&[]),
272 }
273}
274
275/// Checks a node's payload map: no unknown keys, every field correctly typed, every required field
276/// present.
277///
278/// The kind is passed as its label, its field table and its content class rather than as a
279/// `NodeKind`, because a reserved kind (§4.2) has a payload schema of its own and is held to it by
280/// exactly this routine. One check, and no second implementation to drift from the first.
281fn check_fields(
282 id: usize,
283 label: &str,
284 fields: &'static [Field],
285 content: Content,
286 table: bool,
287 map: &DaticleMap,
288)
289 -> Outcome<()>
290{
291 // Every key present is either a declared field or the children list.
292 for (k, v) in map.iter() {
293 let key = match k {
294 Dat::Str(s) => s.as_str(),
295 d => return Err(err!(
296 "Node {} ({}) has a map key of kind {}; every map key is a str.",
297 id, label, d.kind();
298 Invalid, Input)),
299 };
300 if key == KEY_CHILDREN {
301 if content == Content::None {
302 return Err(err!(
303 "Node {} ({}) carries a '{}' field, but a {} admits no children.",
304 id, label, KEY_CHILDREN, label;
305 Invalid, Input));
306 }
307 continue;
308 }
309 // The universal `style` field (§4.4) is legal on any map payload and is resolved against the
310 // document's style table elsewhere, so it is never an unknown field.
311 if key == KEY_STYLE {
312 continue;
313 }
314 // The `styles` table is the doc node's own field (§4.4), validated separately; on any other
315 // kind it falls through and is rejected as unknown below.
316 if key == KEY_STYLES && table {
317 continue;
318 }
319 match fields.iter().find(|f| f.name == key) {
320 Some(f) => res!(check_field(id, label, f, v)),
321 None => return Err(err!(
322 "Node {} ({}) carries the unknown field '{}'; a {} declares {}.",
323 id, label, key, label, field_list(fields);
324 Invalid, Input)),
325 }
326 }
327
328 // Every required field is present.
329 for f in fields {
330 if !f.opt && map.get(&dat!(f.name)).is_none() {
331 return Err(err!(
332 "Node {} ({}) is missing the required field '{}' of type {}.",
333 id, label, f.name, type_label(f.typ);
334 Invalid, Input, Missing));
335 }
336 }
337
338 Ok(())
339}
340
341/// Checks one field's value against the type its schema declares for it.
342fn check_field(
343 id: usize,
344 label: &str,
345 f: &Field,
346 v: &Dat,
347)
348 -> Outcome<()>
349{
350 // An address is a typed sub-structure rather than a scalar, so it is checked by its own routine,
351 // which tells a name from a hash and refuses a malformed target (§4.3).
352 if f.typ == FieldType::Address {
353 return match check_address(v) {
354 Ok(_) => Ok(()),
355 Err(e) => Err(err!(e,
356 "Node {} ({}) field '{}' is not a valid link address.",
357 id, label, f.name;
358 Invalid, Input)),
359 };
360 }
361
362 // A field carrying nodes is a list the walk will validate, node by node, like any other. What is
363 // checked here is that it is a list at all, and that it is not empty: a `surface`'s alternative is
364 // what a screen reader reads, what a search indexes, and what the reader sees when the application
365 // is not running, so an empty one is a hole in the document with nothing behind it (§4.2).
366 if f.typ == FieldType::Nodes {
367 return match v {
368 Dat::List(list) if !list.is_empty() => Ok(()),
369 Dat::List(_) => Err(err!(
370 "Node {} ({}) field '{}' is an empty list; it carries at least one node. An \
371 alternative with nothing in it is a hole with nothing behind it (SPEC.md §4.2).",
372 id, label, f.name;
373 Invalid, Input, Missing)),
374 d => Err(err!(
375 "Node {} ({}) field '{}' is a {} daticle; the schema declares {}.",
376 id, label, f.name, d.kind(), type_label(f.typ);
377 Invalid, Input, Mismatch)),
378 };
379 }
380
381 let typed = match (f.typ, v) {
382 (FieldType::Str, Dat::Str(_)) => true,
383 (FieldType::U8, Dat::U8(_)) => true,
384 (FieldType::I8, Dat::I8(_)) => true,
385 (FieldType::U32, Dat::U32(_)) => true,
386 (FieldType::Bool, Dat::Bool(_)) => true,
387 (FieldType::Hash32, Dat::B32(_)) => true,
388 _ => false,
389 };
390 if !typed {
391 return Err(err!(
392 "Node {} ({}) field '{}' is a {} daticle; the schema declares {}.",
393 id, label, f.name, v.kind(), type_label(f.typ);
394 Invalid, Input, Mismatch));
395 }
396
397 Ok(())
398}
399
400/// Checks a heading's level, the one field with a range as well as a width: it runs from 1 to 6.
401///
402/// The width is already pinned by [`check_field`], so a level that is here at all is a `u8`.
403fn check_heading_level(
404 id: usize,
405 map: &DaticleMap,
406)
407 -> Outcome<()>
408{
409 if let Some(Dat::U8(level)) = map.get(&dat!("level")) {
410 if *level < 1 || *level > 6 {
411 return Err(err!(
412 "Node {} (heading) has level {}, which is outside the range 1..=6.", id, level;
413 Invalid, Input, Range));
414 }
415 }
416 Ok(())
417}
418
419/// The schema name of a field type, as an error message spells it.
420fn type_label(typ: FieldType) -> &'static str {
421 match typ {
422 FieldType::Str => "str",
423 FieldType::U8 => "u8",
424 FieldType::I8 => "i8",
425 FieldType::U32 => "u32",
426 FieldType::Bool => "bool",
427 FieldType::Hash32 => "b32",
428 FieldType::Address => "address",
429 FieldType::Nodes => "a non-empty list of nodes",
430 }
431}
432
433/// The name of a content class, as an error message spells it.
434fn content_label(content: Content) -> &'static str {
435 match content {
436 Content::None => "no",
437 Content::Flow => "flow",
438 Content::Inline => "inline",
439 Content::Items => "item",
440 }
441}
442
443/// What a kind's payload is, as an error message spells it.
444fn payload_label(kind: NodeKind) -> &'static str {
445 if kind.payload_is_str() {
446 "a str"
447 } else {
448 "a map"
449 }
450}
451
452/// The fields a kind declares, listed for an error message.
453fn field_list(fields: &'static [Field]) -> String {
454 if fields.is_empty() {
455 return fmt!("no fields");
456 }
457 let mut s = String::new();
458 for (i, f) in fields.iter().enumerate() {
459 if i > 0 {
460 s.push_str(", ");
461 }
462 s.push_str(&fmt!("'{}': {}{}",
463 f.name,
464 type_label(f.typ),
465 if f.opt { "?" } else { "" },
466 ));
467 }
468 s
469}
470
471/// Validates a node of a reserved kind (§4.2), and pushes a surface's alternative onto the walk.
472///
473/// The refusals here are the security boundary of the whole format, and they are ordered so that the
474/// strongest one is reached first. A reserved kind inside an alternative is refused whatever the
475/// schema; a reserved kind the schema does not admit is refused whatever it carries; and only then is
476/// what it carries looked at all.
477fn check_reserved<'a>(
478 id: usize,
479 reserved: ReservedKind,
480 schema: Schema,
481 parent: Option<NodeKind>,
482 payload_opt: &'a Option<Box<Dat>>,
483 depth: usize,
484 inert: bool,
485 surfaces: &mut usize,
486 styles: &[String],
487 referenced: &mut Vec<String>,
488 stack: &mut Vec<(&'a Dat, Option<NodeKind>, usize, bool)>,
489)
490 -> Outcome<()>
491{
492 // An alternative is inert content, always. It stands in for an application that is not running, so
493 // a live thing inside it would be a hole with another hole behind it, and a surface that could
494 // nest could open one instance per alternative, per instance, without end.
495 if inert {
496 return Err(err!(
497 "Node {}: a surface's alternative may not carry {} node. The alternative is what a \
498 screen reader reads and what the reader sees when the application is not running, so it \
499 carries inert content only, whatever the schema '{}' admits elsewhere (SPEC.md §4.2).",
500 id, reserved.with_article(), schema.name();
501 Invalid, Input, Security));
502 }
503
504 // SPEC §4.2 and §4.5: a code the reader knows, and the schema does not admit, is refused
505 // unconditionally, fallback or no fallback. This is not the fallback rule's business and never
506 // falls through to it.
507 if !schema.admits(reserved) {
508 return Err(err!(
509 "Node {}: the kind code {} is the reserved kind '{}', which is legal in {}. The schema \
510 '{}' admits {}, so {} may not carry {} node, whether or not it carries a fallback \
511 (SPEC.md §4.2, §4.5).",
512 id, reserved.code(), reserved.label(), reserved.legal_in(),
513 schema.name(), schema.admits_label(), schema.tree_label(), reserved.with_article();
514 Invalid, Input, Security));
515 }
516
517 // Where a reserved kind may sit is its own business (§4.2): a field and a pane are blocks and go
518 // where a box goes, and an icon is a glyph and goes where a text run goes. The root is never one,
519 // since the root is always a doc.
520 let pkind = match parent {
521 Some(pkind) => pkind,
522 None => return Err(err!(
523 "Node 0 is {}; the root of an '{}' payload must be a doc.",
524 reserved.with_article(), schema.name();
525 Invalid, Input)),
526 };
527 if pkind.content() != reserved.sits_in() {
528 return Err(err!(
529 "Node {} is {} inside a {}, which admits {} content only. {} is {} content.",
530 id, reserved.with_article(), pkind.label(), content_label(pkind.content()),
531 reserved.with_article(), content_label(reserved.sits_in());
532 Invalid, Input));
533 }
534
535 let payload = match payload_opt {
536 Some(payload) => payload.as_ref(),
537 None => return Err(err!(
538 "Node {} ({}) carries no payload; {} carries a map.",
539 id, reserved.label(), reserved.with_article();
540 Invalid, Input, Missing)),
541 };
542 let map = match payload {
543 Dat::Map(map) => map,
544 d => return Err(err!(
545 "Node {} ({}) carries a {} payload; {} carries a map.",
546 id, reserved.label(), d.kind(), reserved.with_article();
547 Invalid, Input)),
548 };
549
550 // Neither reserved kind carries children, and each has its own reason. The generic refusal in
551 // `check_fields` would catch this too; it is caught here so that the refusal says which reason.
552 if map.get(&dat!(KEY_CHILDREN)).is_some() {
553 return Err(err!(
554 "Node {} ({}) carries a '{}' key, and takes none: {} (SPEC.md §4.2).",
555 id, reserved.label(), KEY_CHILDREN, reserved.no_children();
556 Invalid, Input));
557 }
558
559 res!(check_fields(
560 id, reserved.label(), reserved.fields(), reserved.content(), false, map));
561
562 // The universal `style` field (§4.4) is legal on a reserved kind's map payload like any other.
563 if let Some(sv) = map.get(&dat!(KEY_STYLE)) {
564 res!(check_style_ref(id, reserved.label(), sv, styles));
565 if let Dat::Str(s) = sv {
566 referenced.push(s.clone());
567 }
568 }
569
570 if reserved == ReservedKind::Icon {
571 // The set is closed (§4.2). An icon names the engine's own drawing rather than carrying any,
572 // so a name the engine does not draw has nothing faithful to stand in for it, and is a fault
573 // in whoever built the tree rather than a gap a reader should paper over.
574 let name = match map.get(&dat!("name")) {
575 Some(Dat::Str(s)) => s.clone(),
576 // `check_fields` has already established that the field is present and is a string, so
577 // this arm is unreachable, and it is written out rather than assumed.
578 _ => return Err(err!(
579 "Node {} (icon): the 'name' field is not the string the schema requires, and the \
580 field check did not catch it. This is a fault in the validator.", id;
581 Bug, Invalid)),
582 };
583 if !known_icon(&name) {
584 return Err(err!(
585 "Node {} (icon): '{}' is not an icon this engine draws. The set is closed at '{}' \
586 (SPEC.md §4.2).", id, name, icon_names_label();
587 Invalid, Input, Mismatch));
588 }
589 }
590
591 if reserved == ReservedKind::Surface {
592 *surfaces += 1;
593 if *surfaces > limit::SURFACES {
594 return Err(err!(
595 "Node {} (surface): the tree carries more than {} surfaces, which is the ceiling \
596 (SPEC.md §5). Every surface is a live application instance the host must lay out, \
597 budget and present.", id, limit::SURFACES;
598 Invalid, Input, TooBig, LimitReached));
599 }
600 // The alternative is walked as ordinary content, under the surface's own parent, so its nodes
601 // meet exactly the child rules that governed where the surface sat, and are counted and
602 // numbered like any others. They are marked inert: nothing beneath them may come alive.
603 let alt = match map.get(&dat!(KEY_ALT)) {
604 Some(Dat::List(alt)) => alt,
605 // `check_fields` has already established that the field is present and is a non-empty
606 // list, so this arm is unreachable, and it is written out rather than assumed.
607 _ => return Err(err!(
608 "Node {} (surface): the '{}' field is not the non-empty list of nodes the schema \
609 requires, and the field check did not catch it. This is a fault in the validator.",
610 id, KEY_ALT;
611 Bug, Invalid)),
612 };
613 for kid in alt.iter().rev() {
614 stack.push((kid, parent, depth + 1, true));
615 }
616 }
617
618 Ok(())
619}
620
621/// Validates an unknown kind's fallback and pushes its children onto the walk (§4.5).
622///
623/// An unknown kind is permitted only when its payload is a map carrying a non-empty `fallback` list,
624/// which the reader renders and validates in the unknown node's place. The children are pushed under
625/// the unknown node's own parent, so they meet exactly the child rules that governed where the
626/// unknown node sat, and they are counted and numbered as ordinary nodes when the walk reaches them.
627/// The unknown kind's other fields are not interpreted here.
628///
629/// A fallback inside an alternative stays inert, so a kind the reader has never heard of cannot smuggle
630/// a live node into the content that stands in for an application.
631fn push_fallback<'a>(
632 id: usize,
633 code: u16,
634 parent: Option<NodeKind>,
635 payload_opt: &'a Option<Box<Dat>>,
636 depth: usize,
637 inert: bool,
638 stack: &mut Vec<(&'a Dat, Option<NodeKind>, usize, bool)>,
639)
640 -> Outcome<()>
641{
642 let payload = match payload_opt {
643 Some(payload) => payload.as_ref(),
644 None => return Err(err!(
645 "Node {}: the unknown kind code {} carries no payload; an unknown kind is permitted \
646 only when its payload is a map carrying a non-empty '{}' (SPEC.md §4.5).",
647 id, code, KEY_FALLBACK;
648 Invalid, Input, Missing)),
649 };
650 let map = match payload {
651 Dat::Map(map) => map,
652 d => return Err(err!(
653 "Node {}: the unknown kind code {} carries a {} payload; an unknown kind is permitted \
654 only when its payload is a map carrying a non-empty '{}' (SPEC.md §4.5).",
655 id, code, d.kind(), KEY_FALLBACK;
656 Invalid, Input)),
657 };
658 let list = match map.get(&dat!(KEY_FALLBACK)) {
659 Some(Dat::List(list)) if !list.is_empty() => list,
660 Some(Dat::List(_)) => return Err(err!(
661 "Node {}: the unknown kind code {} carries an empty '{}'; an unknown kind is permitted \
662 only with a non-empty fallback of known nodes (SPEC.md §4.5).",
663 id, code, KEY_FALLBACK;
664 Invalid, Input)),
665 Some(d) => return Err(err!(
666 "Node {}: the unknown kind code {} carries a '{}' of kind {}; a fallback is a \
667 non-empty list of known nodes (SPEC.md §4.5).",
668 id, code, KEY_FALLBACK, d.kind();
669 Invalid, Input)),
670 None => return Err(err!(
671 "Node {}: the unknown kind code {} carries no '{}'; an unknown kind is permitted only \
672 when its payload is a map carrying a non-empty fallback of known nodes (SPEC.md §4.5).",
673 id, code, KEY_FALLBACK;
674 Invalid, Input, Missing)),
675 };
676 for child in list.iter().rev() {
677 stack.push((child, parent, depth + 1, inert));
678 }
679 Ok(())
680}
681
682/// Validates the doc node's style table, returning the style names it defines (§4.4).
683///
684/// The table is a map from style name to style record. Each key must be a str, and each value a
685/// record whose properties are all known and correctly valued. An absent table is simply an empty
686/// set of names.
687fn check_styles_table(
688 id: usize,
689 schema: Schema,
690 map: &DaticleMap,
691)
692 -> Outcome<Vec<String>>
693{
694 let table = match map.get(&dat!(KEY_STYLES)) {
695 None => return Ok(Vec::new()),
696 Some(Dat::Map(table)) => table,
697 Some(d) => return Err(err!(
698 "Node {} (doc): the '{}' style table is a map from name to style record, found a {} \
699 (SPEC.md §4.4).", id, KEY_STYLES, d.kind();
700 Invalid, Input)),
701 };
702 let mut names = Vec::with_capacity(table.len());
703 for (k, v) in table.iter() {
704 let name = match k {
705 Dat::Str(s) => s.as_str(),
706 d => return Err(err!(
707 "Node {} (doc): the '{}' table has a key of kind {}; a style name is a str \
708 (SPEC.md §4.4).", id, KEY_STYLES, d.kind();
709 Invalid, Input)),
710 };
711 let record = match v {
712 Dat::Map(record) => record,
713 d => return Err(err!(
714 "Node {} (doc): style '{}' is a {}; a style record is a map of style properties \
715 (SPEC.md §4.4).", id, name, d.kind();
716 Invalid, Input)),
717 };
718 res!(check_style_record(id, schema, name, record));
719 names.push(name.to_string());
720 }
721 Ok(names)
722}
723
724/// Validates one style record: every property the schema admits, every value satisfying its check
725/// (§4.4).
726///
727/// A property this schema does not admit and a property that does not exist are refused by the same
728/// rule and named by different messages, because they are different mistakes. "Unknown style property
729/// 'grid'" told to an author whose chrome draws a grid every day would be a lie, so the refusal says
730/// which schema turned it down and where it would have been legal.
731fn check_style_record(
732 id: usize,
733 schema: Schema,
734 name: &str,
735 record: &DaticleMap,
736)
737 -> Outcome<()>
738{
739 for (k, v) in record.iter() {
740 let prop = match k {
741 Dat::Str(s) => s.as_str(),
742 d => return Err(err!(
743 "Node {} (doc): style '{}' has a property key of kind {}; a style property name \
744 is a str (SPEC.md §4.4).", id, name, d.kind();
745 Invalid, Input)),
746 };
747 let field = match schema.style_field(prop) {
748 Some(field) => field,
749 // The property is a real one, and this schema does not admit it. A document naming an
750 // interface property is refused here, and this is the whole of the reason: the v0 document
751 // vocabulary is frozen at eight properties, so a document cannot dress as an interface, and
752 // cannot grow into one by waiting for a reader that draws more.
753 None => match known_style_field(prop) {
754 Some(known) => return Err(err!(
755 "Node {} (doc): style '{}' names the style property '{}', which is legal in {}. \
756 The schema '{}' admits {}, so {} may not name it (SPEC.md §4.4).",
757 id, name, prop, known.scope.legal_in(),
758 schema.name(), schema.style_admits_label(), schema.tree_label();
759 Invalid, Input)),
760 None => return Err(err!(
761 "Node {} (doc): style '{}' carries the unknown style property '{}' (SPEC.md \
762 §4.4).", id, name, prop;
763 Invalid, Input)),
764 },
765 };
766 res!(check_style_value(id, name, field, v));
767 }
768
769 // `grid` and `pack` are two layouts, and a style naming both has asked for both. A renderer must
770 // then pick one, and which it picks is a fact about that renderer rather than about the tree -- so
771 // two readers could lay the same legal chrome out differently and both be right, which is the
772 // thing a frozen format exists to prevent. Refusing the pair keeps the tree's meaning the tree's.
773 //
774 // This is the only rule here that reads a style record as a WHOLE rather than a property at a
775 // time, because it is the only one about a combination.
776 if record.contains_key(&dat!("grid")) && record.contains_key(&dat!("pack")) {
777 return Err(err!(
778 "Node {} (doc): style '{}' names both 'grid' and 'pack'. A grid shares its width out \
779 among its tiles and a packed row leaves each at the width it names, so a style naming \
780 both has asked for two different layouts and no reader can honour it (SPEC.md §4.4).",
781 id, name;
782 Invalid, Input));
783 }
784 Ok(())
785}
786
787/// Validates one style-record value against the enum or width its property declares (§4.4).
788fn check_style_value(
789 id: usize,
790 name: &str,
791 field: &StyleField,
792 v: &Dat,
793)
794 -> Outcome<()>
795{
796 // A border is a typed sub-structure rather than a scalar, so it is checked by its own routine,
797 // exactly as a link address is (§4.3), and a malformed one is refused here rather than misread by
798 // the renderer.
799 if field.check == StyleCheck::Border {
800 return match check_border(v) {
801 Ok(_) => Ok(()),
802 Err(e) => Err(err!(e,
803 "Node {} (doc): style '{}' property '{}' is not a valid border (SPEC.md §4.4).",
804 id, name, field.name;
805 Invalid, Input)),
806 };
807 }
808
809 let ok = match field.check {
810 StyleCheck::Palette => matches!(v, Dat::Str(s) if PALETTE.contains(&s.as_str())),
811 StyleCheck::Direction => matches!(v, Dat::Str(s) if DIRECTIONS.contains(&s.as_str())),
812 StyleCheck::Alignment => matches!(v, Dat::Str(s) if ALIGNMENTS.contains(&s.as_str())),
813 StyleCheck::ScaleStep => matches!(v, Dat::I8(_)),
814 StyleCheck::Spacing => matches!(v, Dat::U8(_)),
815 StyleCheck::Lang => matches!(v, Dat::Str(_)),
816 StyleCheck::Tile => matches!(v, Dat::U16(_)),
817 StyleCheck::Share => matches!(v, Dat::U8(_)),
818 StyleCheck::Elevation => matches!(v, Dat::U8(_)),
819 // Checked above, and written out rather than caught by a wildcard, so that a check added later
820 // and forgotten here is a compile error and not an accepted value.
821 StyleCheck::Border => false,
822 };
823 if !ok {
824 return Err(err!(
825 "Node {} (doc): style '{}' property '{}' carries {}; the schema requires {} \
826 (SPEC.md §4.4).",
827 id, name, field.name, style_value_label(v), style_check_label(field.check);
828 Invalid, Input));
829 }
830 Ok(())
831}
832
833/// A style reference names a str that resolves to an entry in the document's style table (§4.4).
834fn check_style_ref(
835 id: usize,
836 label: &str,
837 sv: &Dat,
838 styles: &[String],
839)
840 -> Outcome<()>
841{
842 let name = match sv {
843 Dat::Str(s) => s.as_str(),
844 d => return Err(err!(
845 "Node {} ({}): the '{}' field is a {}; a style reference is a str naming an entry in \
846 the document's style table (SPEC.md §4.4).",
847 id, label, KEY_STYLE, d.kind();
848 Invalid, Input)),
849 };
850 if !styles.iter().any(|n| n == name) {
851 return Err(err!(
852 "Node {} ({}): the '{}' field names the style '{}', which the document's style table \
853 does not define (SPEC.md §4.4).",
854 id, label, KEY_STYLE, name;
855 Invalid, Input));
856 }
857 Ok(())
858}
859
860/// Describes a style value for an error message, spelling out a string or scalar and naming a kind.
861fn style_value_label(v: &Dat) -> String {
862 match v {
863 Dat::Str(s) => fmt!("the string \"{}\"", s),
864 Dat::U8(n) => fmt!("the u8 {}", n),
865 Dat::I8(n) => fmt!("the i8 {}", n),
866 other => fmt!("a {} daticle", other.kind()),
867 }
868}
869
870/// Names what a style check permits, for an error message.
871fn style_check_label(check: StyleCheck) -> String {
872 match check {
873 StyleCheck::Palette => fmt!("a palette name ({})", PALETTE.join(", ")),
874 StyleCheck::Direction => fmt!("a direction ({})", DIRECTIONS.join(", ")),
875 StyleCheck::Alignment => fmt!("an alignment ({})", ALIGNMENTS.join(", ")),
876 StyleCheck::ScaleStep => fmt!("a type scale step (i8)"),
877 StyleCheck::Spacing => fmt!("a spacing index (u8)"),
878 StyleCheck::Lang => fmt!("a language tag (str)"),
879 StyleCheck::Tile => fmt!("a grid tile width as a percentage of a base size (u16)"),
880 StyleCheck::Share => fmt!("a share of a packed row's leftover room (u8)"),
881 StyleCheck::Elevation => fmt!("an elevation, in whole steps off the surface behind (u8)"),
882 StyleCheck::Border => fmt!("a palette name and a width in pixels ([str, u8])"),
883 }
884}
885
886#[cfg(test)]
887mod tests {
888 use super::*;
889 use crate::{
890 SCHEMA_APP,
891 SCHEMA_CHROME,
892 SCHEMA_DOC,
893 };
894
895 use oxedyne_fe2o3_jdat::usr::UsrKindId;
896
897 /// Builds a node of the given kind with the given payload.
898 fn node(kind: NodeKind, payload: Dat) -> Dat {
899 Dat::Usr(
900 UsrKindId::new(kind.code(), Some(kind.label()), None),
901 Some(Box::new(payload)),
902 )
903 }
904
905 /// Builds a text run.
906 fn text(s: &str) -> Dat {
907 node(NodeKind::Text, dat!(s))
908 }
909
910 /// Builds a 32-byte content hash, the width of a v0 hash reference.
911 fn hash32() -> Dat {
912 Dat::B32(B32([0x9fu8; 32]))
913 }
914
915 /// A style may name a grid or a packed row, and not both.
916 ///
917 /// The two are different layouts of the same children, so a style naming both leaves the reader to
918 /// choose, and a legal tree whose appearance depends on which reader drew it is the thing a frozen
919 /// format exists to prevent. Each ALONE is legal chrome, which is what says the pair is refused for
920 /// being a pair rather than either being wrong.
921 #[test]
922 fn test_a_style_naming_both_a_grid_and_a_packed_row_is_refused_33() -> Outcome<()> {
923 let tree = |props: Dat| -> Dat {
924 node(NodeKind::Doc, mapdat!{
925 "title" => dat!("A Title"),
926 "lang" => dat!("en"),
927 "styles" => mapdat!{ "shelf" => props },
928 "children" => Dat::List(vec![
929 node(NodeKind::Boxx, mapdat!{
930 "style" => dat!("shelf"),
931 "children" => Dat::List(vec![
932 node(NodeKind::Para, mapdat!{ "children" => Dat::List(vec![text("a")]) }),
933 ]),
934 }),
935 ]),
936 })
937 };
938 // Either alone is legal chrome.
939 res!(validate(&tree(mapdat!{ "grid" => dat!(1400u16) }), SCHEMA_CHROME));
940 res!(validate(&tree(mapdat!{ "pack" => dat!(600u16) }), SCHEMA_CHROME));
941
942 // Both together is not, in any schema that admits them at all.
943 for schema in [SCHEMA_CHROME, SCHEMA_APP] {
944 assert!(
945 validate(&tree(mapdat!{ "grid" => dat!(1400u16), "pack" => dat!(600u16) }), schema).is_err(),
946 "a style naming both a grid and a packed row is refused in {}", schema,
947 );
948 }
949 // And a document may name neither, pair or no pair.
950 assert!(
951 validate(&tree(mapdat!{ "pack" => dat!(600u16) }), SCHEMA_DOC).is_err(),
952 "a document may not name a packed row: it is an interface property",
953 );
954 Ok(())
955 }
956
957 /// Builds a document whose children are the given flow nodes.
958 fn doc(kids: Vec<Dat>) -> Dat {
959 node(NodeKind::Doc, mapdat!{
960 "title" => dat!("A Title"),
961 "lang" => dat!("en"),
962 "children" => Dat::List(kids),
963 })
964 }
965
966 /// A document using every v0 node kind at least once.
967 fn every_kind() -> Dat {
968 let heading = node(NodeKind::Heading, mapdat!{
969 "level" => dat!(2u8),
970 "children" => Dat::List(vec![text("Style without a cascade")]),
971 });
972 let para = node(NodeKind::Para, mapdat!{
973 "children" => Dat::List(vec![
974 text("A run, "),
975 node(NodeKind::Emph, mapdat!{
976 "strong" => dat!(true),
977 "children" => Dat::List(vec![text("emphasised")]),
978 }),
979 node(NodeKind::Link, mapdat!{
980 "to" => mapdat!{ "name" => dat!("news.cricket") },
981 "children" => Dat::List(vec![text("a link")]),
982 }),
983 ]),
984 });
985 let list = node(NodeKind::List, mapdat!{
986 "ordered" => dat!(false),
987 "children" => Dat::List(vec![
988 node(NodeKind::Item, mapdat!{
989 "children" => Dat::List(vec![
990 node(NodeKind::Para, mapdat!{
991 "children" => Dat::List(vec![text("An item")]),
992 }),
993 ]),
994 }),
995 ]),
996 });
997 let image = node(NodeKind::Image, mapdat!{
998 "hash" => hash32(),
999 "alt" => dat!("A diagram of the tree"),
1000 "w" => dat!(640u32),
1001 "h" => dat!(480u32),
1002 });
1003 let boxx = node(NodeKind::Boxx, mapdat!{
1004 "style" => dat!("note"),
1005 "children" => Dat::List(vec![image]),
1006 });
1007 let section = node(NodeKind::Section, mapdat!{
1008 "title" => dat!("A Section"),
1009 "children" => Dat::List(vec![heading, para, list, boxx]),
1010 });
1011 // The doc carries a style table so the box's "note" style resolves (§4.4).
1012 node(NodeKind::Doc, mapdat!{
1013 "title" => dat!("A Title"),
1014 "lang" => dat!("en"),
1015 "styles" => mapdat!{
1016 "note" => mapdat!{ "bg" => dat!("muted"), "pad" => dat!(3u8) },
1017 },
1018 "children" => Dat::List(vec![section]),
1019 })
1020 }
1021
1022 #[test]
1023 fn test_valid_every_kind_00() -> Outcome<()> {
1024 let stats = res!(validate(&every_kind(), SCHEMA_DOC));
1025 // doc, section, heading, text, para, text, emph, text, link, text, list, item, para, text,
1026 // box, image.
1027 assert_eq!(stats.nodes, 16);
1028 // The deepest run is doc, section, list, item, para, text.
1029 assert_eq!(stats.depth, 6);
1030 Ok(())
1031 }
1032
1033 #[test]
1034 fn test_wrong_schema_01() -> Outcome<()> {
1035 let e = match validate(&every_kind(), "oxeweb/doc/1") {
1036 Ok(_) => return Err(err!("A foreign schema was accepted."; Test)),
1037 Err(e) => fmt!("{}", e),
1038 };
1039 assert!(e.contains("oxeweb/doc/1"), "Error must name the schema it got: {}", e);
1040 Ok(())
1041 }
1042
1043 #[test]
1044 fn test_para_in_para_02() -> Outcome<()> {
1045 let inner = node(NodeKind::Para, mapdat!{
1046 "children" => Dat::List(vec![text("Inner")]),
1047 });
1048 let outer = node(NodeKind::Para, mapdat!{
1049 "children" => Dat::List(vec![inner]),
1050 });
1051 let e = match validate(&doc(vec![outer]), SCHEMA_DOC) {
1052 Ok(_) => return Err(err!("A para inside a para was accepted."; Test)),
1053 Err(e) => fmt!("{}", e),
1054 };
1055 assert!(e.contains("Node 2"), "Error must name node 2: {}", e);
1056 assert!(e.contains("para"), "Error must name the kind: {}", e);
1057 assert!(e.contains("inline"), "Error must name the rule broken: {}", e);
1058 Ok(())
1059 }
1060
1061 #[test]
1062 fn test_unknown_kind_code_03() -> Outcome<()> {
1063 let alien = Dat::Usr(
1064 UsrKindId::new(99, None, None),
1065 Some(Box::new(mapdat!{})),
1066 );
1067 let e = match validate(&doc(vec![alien]), SCHEMA_DOC) {
1068 Ok(_) => return Err(err!("An unknown kind code was accepted."; Test)),
1069 Err(e) => fmt!("{}", e),
1070 };
1071 assert!(e.contains("Node 1"), "Error must name node 1: {}", e);
1072 assert!(e.contains("99"), "Error must name the code: {}", e);
1073 Ok(())
1074 }
1075
1076 #[test]
1077 fn test_missing_required_field_04() -> Outcome<()> {
1078 // A list without its ordered field.
1079 let list = node(NodeKind::List, mapdat!{
1080 "children" => Dat::List(vec![
1081 node(NodeKind::Item, mapdat!{}),
1082 ]),
1083 });
1084 let e = match validate(&doc(vec![list]), SCHEMA_DOC) {
1085 Ok(_) => return Err(err!("A list without 'ordered' was accepted."; Test)),
1086 Err(e) => fmt!("{}", e),
1087 };
1088 assert!(e.contains("Node 1"), "Error must name node 1: {}", e);
1089 assert!(e.contains("ordered"), "Error must name the field: {}", e);
1090 Ok(())
1091 }
1092
1093 #[test]
1094 fn test_unknown_field_05() -> Outcome<()> {
1095 let para = node(NodeKind::Para, mapdat!{
1096 "align" => dat!("centre"),
1097 "children" => Dat::List(vec![text("Text")]),
1098 });
1099 let e = match validate(&doc(vec![para]), SCHEMA_DOC) {
1100 Ok(_) => return Err(err!("An unknown field was accepted."; Test)),
1101 Err(e) => fmt!("{}", e),
1102 };
1103 assert!(e.contains("Node 1"), "Error must name node 1: {}", e);
1104 assert!(e.contains("align"), "Error must name the field: {}", e);
1105 Ok(())
1106 }
1107
1108 #[test]
1109 fn test_wrong_typed_field_06() -> Outcome<()> {
1110 // A heading level as an i32, not the declared u8: two encodings, two addresses.
1111 let heading = node(NodeKind::Heading, mapdat!{
1112 "level" => dat!(2i32),
1113 "children" => Dat::List(vec![text("Heading")]),
1114 });
1115 let e = match validate(&doc(vec![heading]), SCHEMA_DOC) {
1116 Ok(_) => return Err(err!("A wrongly typed field was accepted."; Test)),
1117 Err(e) => fmt!("{}", e),
1118 };
1119 assert!(e.contains("Node 1"), "Error must name node 1: {}", e);
1120 assert!(e.contains("level"), "Error must name the field: {}", e);
1121 assert!(e.contains("u8"), "Error must name the declared type: {}", e);
1122 Ok(())
1123 }
1124
1125 #[test]
1126 fn test_heading_level_zero_07() -> Outcome<()> {
1127 let heading = node(NodeKind::Heading, mapdat!{
1128 "level" => dat!(0u8),
1129 "children" => Dat::List(vec![text("Heading")]),
1130 });
1131 let e = match validate(&doc(vec![heading]), SCHEMA_DOC) {
1132 Ok(_) => return Err(err!("A heading of level 0 was accepted."; Test)),
1133 Err(e) => fmt!("{}", e),
1134 };
1135 assert!(e.contains("Node 1"), "Error must name node 1: {}", e);
1136 assert!(e.contains("1..=6"), "Error must name the range: {}", e);
1137 Ok(())
1138 }
1139
1140 #[test]
1141 fn test_heading_level_seven_08() -> Outcome<()> {
1142 let heading = node(NodeKind::Heading, mapdat!{
1143 "level" => dat!(7u8),
1144 "children" => Dat::List(vec![text("Heading")]),
1145 });
1146 let e = match validate(&doc(vec![heading]), SCHEMA_DOC) {
1147 Ok(_) => return Err(err!("A heading of level 7 was accepted."; Test)),
1148 Err(e) => fmt!("{}", e),
1149 };
1150 assert!(e.contains("Node 1"), "Error must name node 1: {}", e);
1151 assert!(e.contains("1..=6"), "Error must name the range: {}", e);
1152 Ok(())
1153 }
1154
1155 #[test]
1156 fn test_heading_levels_one_to_six_09() -> Outcome<()> {
1157 for level in 1u8..=6 {
1158 let heading = node(NodeKind::Heading, mapdat!{
1159 "level" => dat!(level),
1160 "children" => Dat::List(vec![text("Heading")]),
1161 });
1162 let stats = res!(validate(&doc(vec![heading]), SCHEMA_DOC));
1163 assert_eq!(stats.nodes, 3);
1164 }
1165 Ok(())
1166 }
1167
1168 #[test]
1169 fn test_missing_alt_10() -> Outcome<()> {
1170 let image = node(NodeKind::Image, mapdat!{
1171 "hash" => hash32(),
1172 });
1173 let e = match validate(&doc(vec![image]), SCHEMA_DOC) {
1174 Ok(_) => return Err(err!("An image without alt text was accepted."; Test)),
1175 Err(e) => fmt!("{}", e),
1176 };
1177 assert!(e.contains("Node 1"), "Error must name node 1: {}", e);
1178 assert!(e.contains("alt"), "Error must name the field: {}", e);
1179 Ok(())
1180 }
1181
1182 #[test]
1183 fn test_node_count_over_limit_11() -> Outcome<()> {
1184 // One doc plus enough paragraphs to pass the limit.
1185 let mut kids = Vec::new();
1186 for _ in 0..limit::NODES {
1187 kids.push(node(NodeKind::Para, mapdat!{}));
1188 }
1189 let e = match validate(&doc(kids), SCHEMA_DOC) {
1190 Ok(_) => return Err(err!("A tree over the node limit was accepted."; Test)),
1191 Err(e) => fmt!("{}", e),
1192 };
1193 assert!(e.contains(&fmt!("{}", limit::NODES)), "Error must name the limit: {}", e);
1194 Ok(())
1195 }
1196
1197 #[test]
1198 fn test_node_count_at_limit_12() -> Outcome<()> {
1199 let mut kids = Vec::new();
1200 for _ in 0..(limit::NODES - 1) {
1201 kids.push(node(NodeKind::Para, mapdat!{}));
1202 }
1203 let stats = res!(validate(&doc(kids), SCHEMA_DOC));
1204 assert_eq!(stats.nodes, limit::NODES);
1205 Ok(())
1206 }
1207
1208 #[test]
1209 fn test_non_doc_root_13() -> Outcome<()> {
1210 let root = node(NodeKind::Para, mapdat!{
1211 "children" => Dat::List(vec![text("Not a document")]),
1212 });
1213 let e = match validate(&root, SCHEMA_DOC) {
1214 Ok(_) => return Err(err!("A para as root was accepted."; Test)),
1215 Err(e) => fmt!("{}", e),
1216 };
1217 assert!(e.contains("Node 0"), "Error must name node 0: {}", e);
1218 assert!(e.contains("doc"), "Error must name the rule broken: {}", e);
1219 Ok(())
1220 }
1221
1222 #[test]
1223 fn test_text_payload_must_be_str_14() -> Outcome<()> {
1224 let para = node(NodeKind::Para, mapdat!{
1225 "children" => Dat::List(vec![
1226 node(NodeKind::Text, mapdat!{ "value" => dat!("Wrapped") }),
1227 ]),
1228 });
1229 let e = match validate(&doc(vec![para]), SCHEMA_DOC) {
1230 Ok(_) => return Err(err!("A text run with a map payload was accepted."; Test)),
1231 Err(e) => fmt!("{}", e),
1232 };
1233 assert!(e.contains("Node 2"), "Error must name node 2: {}", e);
1234 assert!(e.contains("str"), "Error must name the payload rule: {}", e);
1235 Ok(())
1236 }
1237
1238 #[test]
1239 fn test_non_map_payload_15() -> Outcome<()> {
1240 let para = Dat::Usr(
1241 UsrKindId::new(NodeKind::Para.code(), Some("para"), None),
1242 Some(Box::new(dat!("Bare string"))),
1243 );
1244 let e = match validate(&doc(vec![para]), SCHEMA_DOC) {
1245 Ok(_) => return Err(err!("A para with a string payload was accepted."; Test)),
1246 Err(e) => fmt!("{}", e),
1247 };
1248 assert!(e.contains("Node 1"), "Error must name node 1: {}", e);
1249 assert!(e.contains("map"), "Error must name the payload rule: {}", e);
1250 Ok(())
1251 }
1252
1253 #[test]
1254 fn test_children_where_none_admitted_16() -> Outcome<()> {
1255 let image = node(NodeKind::Image, mapdat!{
1256 "hash" => Dat::BU8(vec![1]),
1257 "alt" => dat!("An image"),
1258 "children" => Dat::List(vec![text("Impossible")]),
1259 });
1260 let e = match validate(&doc(vec![image]), SCHEMA_DOC) {
1261 Ok(_) => return Err(err!("An image with children was accepted."; Test)),
1262 Err(e) => fmt!("{}", e),
1263 };
1264 assert!(e.contains("Node 1"), "Error must name node 1: {}", e);
1265 assert!(e.contains("children"), "Error must name the field: {}", e);
1266 Ok(())
1267 }
1268
1269 #[test]
1270 fn test_node_ids_are_pre_order_17() -> Outcome<()> {
1271 // doc(0), section(1), para(2), text(3), para(4), text(5). The forbidden node is the second
1272 // para's child, so a text run misplaced there must be named 5, not 4.
1273 let bad = node(NodeKind::Section, mapdat!{
1274 "title" => dat!("S"),
1275 "children" => Dat::List(vec![
1276 node(NodeKind::Para, mapdat!{
1277 "children" => Dat::List(vec![text("One")]),
1278 }),
1279 node(NodeKind::Para, mapdat!{
1280 "children" => Dat::List(vec![
1281 node(NodeKind::Heading, mapdat!{
1282 "level" => dat!(1u8),
1283 "children" => Dat::List(vec![]),
1284 }),
1285 ]),
1286 }),
1287 ]),
1288 });
1289 let e = match validate(&doc(vec![bad]), SCHEMA_DOC) {
1290 Ok(_) => return Err(err!("A heading inside a para was accepted."; Test)),
1291 Err(e) => fmt!("{}", e),
1292 };
1293 assert!(e.contains("Node 5"), "Error must name node 5: {}", e);
1294 Ok(())
1295 }
1296
1297 #[test]
1298 fn test_non_usr_node_18() -> Outcome<()> {
1299 let e = match validate(&doc(vec![dat!("Loose string")]), SCHEMA_DOC) {
1300 Ok(_) => return Err(err!("A bare string as a node was accepted."; Test)),
1301 Err(e) => fmt!("{}", e),
1302 };
1303 assert!(e.contains("Node 1"), "Error must name node 1: {}", e);
1304 assert!(e.contains("usr"), "Error must name the rule broken: {}", e);
1305 Ok(())
1306 }
1307
1308 /// Builds a doc whose one paragraph carries a single link with the given address.
1309 fn doc_with_link(to: Dat) -> Dat {
1310 doc(vec![
1311 node(NodeKind::Para, mapdat!{
1312 "children" => Dat::List(vec![
1313 node(NodeKind::Link, mapdat!{
1314 "to" => to,
1315 "children" => Dat::List(vec![text("here")]),
1316 }),
1317 ]),
1318 }),
1319 ])
1320 }
1321
1322 /// Builds a doc carrying a style table of one record and one box that names a style.
1323 fn doc_with_style(record: Dat, style_name: &str) -> Dat {
1324 node(NodeKind::Doc, mapdat!{
1325 "title" => dat!("T"),
1326 "lang" => dat!("en"),
1327 "styles" => mapdat!{ "callout" => record },
1328 "children" => Dat::List(vec![
1329 node(NodeKind::Boxx, mapdat!{
1330 "style" => dat!(style_name),
1331 "children" => Dat::List(vec![
1332 node(NodeKind::Para, mapdat!{
1333 "children" => Dat::List(vec![text("in a box")]),
1334 }),
1335 ]),
1336 }),
1337 ]),
1338 })
1339 }
1340
1341 #[test]
1342 fn test_code_accepted_19() -> Outcome<()> {
1343 let code = node(NodeKind::Code, mapdat!{
1344 "lang" => dat!("rust"),
1345 "text" => dat!("fn main() {}"),
1346 });
1347 let stats = res!(validate(&doc(vec![code]), SCHEMA_DOC));
1348 // doc, code.
1349 assert_eq!(stats.nodes, 2);
1350 Ok(())
1351 }
1352
1353 #[test]
1354 fn test_quote_accepted_20() -> Outcome<()> {
1355 let quote = node(NodeKind::Quote, mapdat!{
1356 "cite" => dat!("A. Author"),
1357 "children" => Dat::List(vec![
1358 node(NodeKind::Para, mapdat!{
1359 "children" => Dat::List(vec![text("Quoted")]),
1360 }),
1361 ]),
1362 });
1363 let stats = res!(validate(&doc(vec![quote]), SCHEMA_DOC));
1364 // doc, quote, para, text.
1365 assert_eq!(stats.nodes, 4);
1366 Ok(())
1367 }
1368
1369 #[test]
1370 fn test_link_by_name_accepted_21() -> Outcome<()> {
1371 let tree = doc_with_link(mapdat!{ "name" => dat!("news.cricket") });
1372 let stats = res!(validate(&tree, SCHEMA_DOC));
1373 // doc, para, link, text.
1374 assert_eq!(stats.nodes, 4);
1375 Ok(())
1376 }
1377
1378 #[test]
1379 fn test_link_by_hash_accepted_22() -> Outcome<()> {
1380 let tree = doc_with_link(mapdat!{ "hash" => hash32() });
1381 let stats = res!(validate(&tree, SCHEMA_DOC));
1382 assert_eq!(stats.nodes, 4);
1383 Ok(())
1384 }
1385
1386 #[test]
1387 fn test_link_address_two_entries_23() -> Outcome<()> {
1388 let tree = doc_with_link(mapdat!{
1389 "name" => dat!("news.cricket"),
1390 "hash" => hash32(),
1391 });
1392 let e = match validate(&tree, SCHEMA_DOC) {
1393 Ok(_) => return Err(err!("A two-entry link address was accepted."; Test)),
1394 Err(e) => fmt!("{}", e),
1395 };
1396 assert!(e.contains("Node 2"), "Error must name node 2: {}", e);
1397 assert!(e.contains("to") || e.contains("address"),
1398 "Error must name the address rule: {}", e);
1399 Ok(())
1400 }
1401
1402 #[test]
1403 fn test_style_names_missing_entry_24() -> Outcome<()> {
1404 // The table defines "callout", but the box names "ghost".
1405 let tree = doc_with_style(mapdat!{ "bg" => dat!("muted") }, "ghost");
1406 let e = match validate(&tree, SCHEMA_DOC) {
1407 Ok(_) => return Err(err!("A style naming an absent entry was accepted."; Test)),
1408 Err(e) => fmt!("{}", e),
1409 };
1410 assert!(e.contains("Node 1"), "Error must name node 1, the box: {}", e);
1411 assert!(e.contains("ghost"), "Error must name the missing style: {}", e);
1412 Ok(())
1413 }
1414
1415 #[test]
1416 fn test_style_defined_but_unreferenced() -> Outcome<()> {
1417 // The table defines "callout", but no node names it. An unused style has no effect, so a
1418 // document with it and one without render alike, which would be two addresses for one
1419 // document (§4.4).
1420 let tree = node(NodeKind::Doc, mapdat!{
1421 "title" => dat!("T"),
1422 "lang" => dat!("en"),
1423 "styles" => mapdat!{ "callout" => mapdat!{ "bg" => dat!("muted") } },
1424 "children" => Dat::List(vec![
1425 node(NodeKind::Para, mapdat!{
1426 "children" => Dat::List(vec![text("no style here")]),
1427 }),
1428 ]),
1429 });
1430 let e = match validate(&tree, SCHEMA_DOC) {
1431 Ok(_) => return Err(err!("An unreferenced style entry was accepted."; Test)),
1432 Err(e) => fmt!("{}", e),
1433 };
1434 assert!(e.contains("callout"), "Error must name the unused style: {}", e);
1435 Ok(())
1436 }
1437
1438 #[test]
1439 fn test_style_unknown_property_25() -> Outcome<()> {
1440 let tree = doc_with_style(mapdat!{ "wibble" => dat!("x") }, "callout");
1441 let e = match validate(&tree, SCHEMA_DOC) {
1442 Ok(_) => return Err(err!("A style record with an unknown property was accepted."; Test)),
1443 Err(e) => fmt!("{}", e),
1444 };
1445 // The style table is validated at the doc, node 0.
1446 assert!(e.contains("Node 0"), "Error must name node 0, the doc: {}", e);
1447 assert!(e.contains("callout"), "Error must name the style: {}", e);
1448 assert!(e.contains("wibble"), "Error must name the property: {}", e);
1449 Ok(())
1450 }
1451
1452 #[test]
1453 fn test_style_bg_out_of_palette_26() -> Outcome<()> {
1454 let tree = doc_with_style(mapdat!{ "bg" => dat!("purple") }, "callout");
1455 let e = match validate(&tree, SCHEMA_DOC) {
1456 Ok(_) => return Err(err!("A bg outside the palette was accepted."; Test)),
1457 Err(e) => fmt!("{}", e),
1458 };
1459 assert!(e.contains("Node 0"), "Error must name node 0, the doc: {}", e);
1460 assert!(e.contains("callout"), "Error must name the style: {}", e);
1461 assert!(e.contains("bg"), "Error must name the property: {}", e);
1462 Ok(())
1463 }
1464
1465 #[test]
1466 fn test_style_align_out_of_enum_27() -> Outcome<()> {
1467 let tree = doc_with_style(mapdat!{ "align" => dat!("middle") }, "callout");
1468 let e = match validate(&tree, SCHEMA_DOC) {
1469 Ok(_) => return Err(err!("An align outside the enum was accepted."; Test)),
1470 Err(e) => fmt!("{}", e),
1471 };
1472 assert!(e.contains("Node 0"), "Error must name node 0, the doc: {}", e);
1473 assert!(e.contains("callout"), "Error must name the style: {}", e);
1474 assert!(e.contains("align"), "Error must name the property: {}", e);
1475 Ok(())
1476 }
1477
1478 #[test]
1479 fn test_unknown_kind_with_fallback_accepted_28() -> Outcome<()> {
1480 // A kind 20 the reader does not know, carrying a valid fallback and an uninterpreted field.
1481 let alien = Dat::Usr(
1482 UsrKindId::new(20, None, None),
1483 Some(Box::new(mapdat!{
1484 "fallback" => Dat::List(vec![
1485 node(NodeKind::Para, mapdat!{
1486 "children" => Dat::List(vec![text("A fallback paragraph")]),
1487 }),
1488 ]),
1489 "rows" => dat!("only a reader that knows kind 20 uses this"),
1490 })),
1491 );
1492 let stats = res!(validate(&doc(vec![alien]), SCHEMA_DOC));
1493 // doc, the unknown node, its fallback para, and that para's text all count toward the limit.
1494 assert_eq!(stats.nodes, 4);
1495 Ok(())
1496 }
1497
1498 #[test]
1499 fn test_unknown_kind_without_fallback_rejected_29() -> Outcome<()> {
1500 let alien = Dat::Usr(
1501 UsrKindId::new(20, None, None),
1502 Some(Box::new(mapdat!{ "rows" => dat!("no fallback here") })),
1503 );
1504 let e = match validate(&doc(vec![alien]), SCHEMA_DOC) {
1505 Ok(_) => return Err(err!("An unknown kind with no fallback was accepted."; Test)),
1506 Err(e) => fmt!("{}", e),
1507 };
1508 assert!(e.contains("Node 1"), "Error must name node 1: {}", e);
1509 assert!(e.contains("fallback"), "Error must name the fallback rule: {}", e);
1510 Ok(())
1511 }
1512
1513 #[test]
1514 fn test_unknown_kind_empty_fallback_rejected_30() -> Outcome<()> {
1515 let alien = Dat::Usr(
1516 UsrKindId::new(20, None, None),
1517 Some(Box::new(mapdat!{ "fallback" => Dat::List(Vec::new()) })),
1518 );
1519 let e = match validate(&doc(vec![alien]), SCHEMA_DOC) {
1520 Ok(_) => return Err(err!("An unknown kind with an empty fallback was accepted."; Test)),
1521 Err(e) => fmt!("{}", e),
1522 };
1523 assert!(e.contains("Node 1"), "Error must name node 1: {}", e);
1524 assert!(e.contains("fallback"), "Error must name the fallback rule: {}", e);
1525 Ok(())
1526 }
1527
1528 /// A node of a reserved kind (§4.2), built as a document author would have to build one.
1529 fn reserved(kind: ReservedKind, payload: Dat) -> Dat {
1530 Dat::Usr(
1531 UsrKindId::new(kind.code(), Some(kind.label()), None),
1532 Some(Box::new(payload)),
1533 )
1534 }
1535
1536 #[test]
1537 fn test_reserved_edit_in_doc_rejected_31() -> Outcome<()> {
1538 let edit = reserved(ReservedKind::Edit, mapdat!{
1539 "placeholder" => dat!("Search the oxeweb"),
1540 });
1541 let e = match validate(&doc(vec![edit]), SCHEMA_DOC) {
1542 Ok(_) => return Err(err!("A document carrying an edit node was accepted."; Test)),
1543 Err(e) => fmt!("{}", e),
1544 };
1545 assert!(e.contains("Node 1"), "Error must name node 1: {}", e);
1546 assert!(e.contains("edit"), "Error must name the kind: {}", e);
1547 assert!(e.contains("may not carry an edit node"),
1548 "Error must say a document may not carry it: {}", e);
1549 Ok(())
1550 }
1551
1552 #[test]
1553 fn test_reserved_surface_in_doc_rejected_32() -> Outcome<()> {
1554 let surface = reserved(ReservedKind::Surface, mapdat!{
1555 "app" => dat!("app.modeller"),
1556 });
1557 let e = match validate(&doc(vec![surface]), SCHEMA_DOC) {
1558 Ok(_) => return Err(err!("A document carrying a surface node was accepted."; Test)),
1559 Err(e) => fmt!("{}", e),
1560 };
1561 assert!(e.contains("Node 1"), "Error must name node 1: {}", e);
1562 assert!(e.contains("surface"), "Error must name the kind: {}", e);
1563 assert!(e.contains("may not carry a surface node"),
1564 "Error must say a document may not carry it: {}", e);
1565 Ok(())
1566 }
1567
1568 #[test]
1569 fn test_reserved_surface_with_fallback_still_rejected_33() -> Outcome<()> {
1570 // The hole §4.5 would leave open if a reserved code were treated as merely unknown. The
1571 // fallback is valid, non-empty, and made of known nodes, and it buys the surface nothing: a
1572 // reader that knows what code 15 is refuses it whether or not it is offered a stand-in.
1573 let surface = reserved(ReservedKind::Surface, mapdat!{
1574 "fallback" => Dat::List(vec![
1575 node(NodeKind::Para, mapdat!{
1576 "children" => Dat::List(vec![text("A picture of a teapot.")]),
1577 }),
1578 ]),
1579 "app" => dat!("app.modeller"),
1580 });
1581 let e = match validate(&doc(vec![surface]), SCHEMA_DOC) {
1582 Ok(_) => return Err(err!(
1583 "A document carrying a surface node with a valid fallback was accepted. A fallback \
1584 admits an unknown kind, and never a reserved one."; Test)),
1585 Err(e) => fmt!("{}", e),
1586 };
1587 assert!(e.contains("Node 1"), "Error must name node 1: {}", e);
1588 assert!(e.contains("may not carry a surface node"),
1589 "Error must say a document may not carry it: {}", e);
1590 assert!(e.contains("whether or not it carries a fallback"),
1591 "Error must say the fallback does not admit it: {}", e);
1592 Ok(())
1593 }
1594
1595 /// A valid border, as a chrome writes one: a palette name and a width in pixels.
1596 fn border() -> Dat {
1597 Dat::List(vec![dat!("muted"), dat!(1u8)])
1598 }
1599
1600 #[test]
1601 fn test_style_interface_property_in_doc_rejected_35() -> Outcome<()> {
1602 // The v0 document vocabulary is closed. A document naming an interface property is refused,
1603 // whatever the reader can draw, so a document cannot dress as an interface.
1604 //
1605 // `radius` is NOT among these and belongs with the document's own eight: it decorates a surface
1606 // a document already draws with `bg` and `pad`, and it means something in prose, which the
1607 // layout of a bar does not. `shadow` is elevation -- a paragraph claiming to float above its
1608 // page IS a document dressing as an interface -- and `border` is the line round a control with
1609 // an edge. See [`STYLE_FIELDS`](oxedyne_fe2o3_sbj::kinds::STYLE_FIELDS) for the whole of that argument.
1610 for (prop, value) in [
1611 ("grid", dat!(1400u16)),
1612 ("shadow", dat!(2u8)),
1613 ("border", border()),
1614 ] {
1615 let tree = doc_with_style(mapdat!{ prop => value }, "callout");
1616 let e = match validate(&tree, SCHEMA_DOC) {
1617 Ok(_) => return Err(err!(
1618 "A document naming the interface style property '{}' was accepted. The v0 \
1619 document vocabulary is frozen at eight.", prop; Test)),
1620 Err(e) => fmt!("{}", e),
1621 };
1622 assert!(e.contains("Node 0"), "Error must name node 0, the doc: {}", e);
1623 assert!(e.contains("callout"), "Error must name the style: {}", e);
1624 assert!(e.contains(prop), "Error must name the property: {}", e);
1625 // The refusal must not call a real property unknown: a member whose chrome draws a grid
1626 // every day is being misled by "unknown style property 'grid'".
1627 assert!(!e.contains("unknown"),
1628 "'{}' is a real property, and the refusal must not call it unknown: {}", prop, e);
1629 assert!(e.contains(SCHEMA_DOC), "Error must name the schema that refused it: {}", e);
1630 assert!(e.contains("a chrome or an application tree"),
1631 "Error must say where '{}' is legal: {}", prop, e);
1632 }
1633 Ok(())
1634 }
1635
1636 #[test]
1637 fn test_style_interface_property_in_chrome_and_app_accepted_36() -> Outcome<()> {
1638 // The chrome is a real interface and needs to look like one: a grid of tiles, an edge, a
1639 // rounded corner. An application's tree is drawn by the same engine and admits the same.
1640 for schema in [SCHEMA_CHROME, SCHEMA_APP] {
1641 let tree = doc_with_style(mapdat!{
1642 "grid" => dat!(1400u16),
1643 "radius" => dat!(2u8),
1644 "border" => border(),
1645 "bg" => dat!("muted"),
1646 "pad" => dat!(3u8),
1647 }, "callout");
1648 let stats = res!(validate(&tree, schema));
1649 // doc, box, para, text.
1650 assert_eq!(stats.nodes, 4, "'{}' admits the interface properties", schema);
1651 }
1652 Ok(())
1653 }
1654
1655 #[test]
1656 fn test_style_unknown_property_in_chrome_still_rejected_37() -> Outcome<()> {
1657 // A wider vocabulary is not an open one: the chrome's admitted set is closed like any other.
1658 let tree = doc_with_style(mapdat!{ "wibble" => dat!("x") }, "callout");
1659 let e = match validate(&tree, SCHEMA_CHROME) {
1660 Ok(_) => return Err(err!("A chrome style with an unknown property was accepted."; Test)),
1661 Err(e) => fmt!("{}", e),
1662 };
1663 assert!(e.contains("wibble"), "Error must name the property: {}", e);
1664 assert!(e.contains("unknown"), "An invented property is unknown, and is named so: {}", e);
1665 Ok(())
1666 }
1667
1668 #[test]
1669 fn test_style_document_properties_admitted_by_every_schema_38() -> Outcome<()> {
1670 // The eight are the floor, not the document's alone: a chrome is a document that may say more.
1671 for schema in [SCHEMA_DOC, SCHEMA_CHROME, SCHEMA_APP] {
1672 let tree = doc_with_style(mapdat!{
1673 "fill" => dat!("ink"),
1674 "size" => dat!(1i8),
1675 "lang" => dat!("en-GB"),
1676 "dir" => dat!("ltr"),
1677 "bg" => dat!("muted"),
1678 "pad" => dat!(3u8),
1679 "align" => dat!("start"),
1680 "radius" => dat!(2u8),
1681 }, "callout");
1682 let stats = res!(validate(&tree, schema));
1683 assert_eq!(stats.nodes, 4, "'{}' admits the eight document properties", schema);
1684 }
1685 Ok(())
1686 }
1687
1688 #[test]
1689 fn test_a_document_may_round_the_corner_of_a_box_it_already_draws_44() -> Outcome<()> {
1690 // The question that moved `radius` out of the interface's list: is a corner radius a property a
1691 // DOCUMENT legitimately has? It decorates a surface a document already draws -- a style may name
1692 // `bg` and `pad`, so a tinted padded box is already a thing a document makes -- and it adds no
1693 // element, no geometry and no authority. A soft-cornered callout is typography, not an
1694 // interface.
1695 let callout = doc_with_style(mapdat!{
1696 "bg" => dat!("muted"),
1697 "pad" => dat!(3u8),
1698 "radius" => dat!(2u8),
1699 }, "callout");
1700 let stats = res!(validate(&callout, SCHEMA_DOC));
1701 assert_eq!(stats.nodes, 4, "a document rounds the corner of its own box");
1702
1703 // And the rule it seemed to break holds where it was actually doing work: a document still
1704 // cannot claim to stand off its page, nor draw the edge of a control.
1705 for prop in ["shadow", "grow"] {
1706 let tree = doc_with_style(mapdat!{ prop => dat!(2u8) }, "callout");
1707 assert!(
1708 validate(&tree, SCHEMA_DOC).is_err(),
1709 "'{}' is the interface's and a document must not name it", prop,
1710 );
1711 }
1712 Ok(())
1713 }
1714
1715 #[test]
1716 fn test_style_border_is_a_palette_name_and_a_width_39() -> Outcome<()> {
1717 // The validator and the renderer must agree about what a border is, since a border the
1718 // validator accepts and Kiln cannot draw is a chrome tree that passes and then fails. These
1719 // are the shapes Kiln refuses, and they are refused here too.
1720 let bad: Vec<(&str, Dat)> = vec![
1721 ("a border of one thing", Dat::List(vec![dat!("muted")])),
1722 ("a border of three", Dat::List(vec![dat!("muted"), dat!(1u8), dat!(2u8)])),
1723 ("a border of nothing", Dat::List(Vec::new())),
1724 ("a border the wrong way round", Dat::List(vec![dat!(1u8), dat!("muted")])),
1725 ("a border that is a bare colour", dat!("muted")),
1726 ("a border of a colour outside the palette", Dat::List(vec![dat!("puce"), dat!(1u8)])),
1727 ("a border whose width is not a u8", Dat::List(vec![dat!("muted"), dat!(1i8)])),
1728 ];
1729 for (what, value) in bad {
1730 let tree = doc_with_style(mapdat!{ "border" => value }, "callout");
1731 let e = match validate(&tree, SCHEMA_CHROME) {
1732 Ok(_) => return Err(err!("{} was accepted, and is not a border.", what; Test)),
1733 Err(e) => fmt!("{}", e),
1734 };
1735 assert!(e.contains("border"), "{}: the error must name the property: {}", what, e);
1736 }
1737 // And the one shape that is a border is accepted.
1738 let tree = doc_with_style(mapdat!{ "border" => border() }, "callout");
1739 res!(validate(&tree, SCHEMA_CHROME));
1740 Ok(())
1741 }
1742
1743 #[test]
1744 fn test_style_grid_and_radius_are_whole_scalars_40() -> Outcome<()> {
1745 // Kiln reads each at ONE declared width and nothing else, so a negative step or a string is
1746 // refused here rather than reaching a renderer that cannot read it. The widths differ -- a grid
1747 // tile is a percentage of a base size and a radius a step on the spacing scale -- and the error
1748 // must name the one it wanted, or an author cannot tell which they got wrong.
1749 for (prop, width) in [("grid", "u16"), ("radius", "u8")] {
1750 for value in [dat!("2"), dat!(-2i8), dat!(2i32), dat!(2u32)] {
1751 let tree = doc_with_style(mapdat!{ prop => value }, "callout");
1752 let e = match validate(&tree, SCHEMA_CHROME) {
1753 Ok(_) => return Err(err!(
1754 "A '{}' that is not a whole scalar was accepted.", prop; Test)),
1755 Err(e) => fmt!("{}", e),
1756 };
1757 assert!(e.contains(prop), "Error must name the property: {}", e);
1758 assert!(e.contains(width), "Error must name the declared width: {}", e);
1759 }
1760 }
1761 Ok(())
1762 }
1763
1764 #[test]
1765 fn test_reserved_icon_in_doc_rejected_41() -> Outcome<()> {
1766 // An icon names the reader's own drawing, so a document carrying one would let whichever
1767 // reader opened it supply the document's content. Refused by the same rule as the other two.
1768 let icon = reserved(ReservedKind::Icon, mapdat!{ "name" => dat!("home") });
1769 let tree = doc(vec![node(NodeKind::Para, mapdat!{ "children" => Dat::List(vec![icon]) })]);
1770 let e = match validate(&tree, SCHEMA_DOC) {
1771 Ok(_) => return Err(err!("A document carrying an icon node was accepted."; Test)),
1772 Err(e) => fmt!("{}", e),
1773 };
1774 // doc 0, para 1, icon 2.
1775 assert!(e.contains("Node 2"), "Error must name the icon's node: {}", e);
1776 assert!(e.contains("icon"), "Error must name the kind: {}", e);
1777 assert!(e.contains("may not carry an icon node"),
1778 "Error must say a document may not carry it: {}", e);
1779 Ok(())
1780 }
1781
1782 #[test]
1783 fn test_reserved_icon_in_chrome_and_app_accepted_42() -> Outcome<()> {
1784 // A chrome is a real interface and its buttons are icons. An application's tree is drawn by
1785 // the same engine and reaches the same set.
1786 for schema in [SCHEMA_CHROME, SCHEMA_APP] {
1787 for name in crate::kinds::ICON_NAMES {
1788 let icon = reserved(ReservedKind::Icon, mapdat!{ "name" => dat!(*name) });
1789 let tree = doc(vec![
1790 node(NodeKind::Para, mapdat!{ "children" => Dat::List(vec![icon]) }),
1791 ]);
1792 let stats = res!(validate(&tree, schema));
1793 // doc, para, icon.
1794 assert_eq!(stats.nodes, 3, "'{}' admits the icon '{}'", schema, name);
1795 }
1796 }
1797 Ok(())
1798 }
1799
1800 #[test]
1801 fn test_reserved_icon_of_an_unknown_name_rejected_43() -> Outcome<()> {
1802 // The set is closed. An icon carries no drawing of its own, so a name the engine has never
1803 // heard of has nothing faithful to stand in for it: there is no gap to render, only a caller
1804 // that asked for something that does not exist.
1805 for schema in [SCHEMA_CHROME, SCHEMA_APP] {
1806 let icon = reserved(ReservedKind::Icon, mapdat!{ "name" => dat!("wibble") });
1807 let tree = doc(vec![node(NodeKind::Para, mapdat!{ "children" => Dat::List(vec![icon]) })]);
1808 let e = match validate(&tree, schema) {
1809 Ok(_) => return Err(err!(
1810 "An icon named 'wibble' was accepted by '{}'.", schema; Test)),
1811 Err(e) => fmt!("{}", e),
1812 };
1813 assert!(e.contains("wibble"), "Error must name the icon asked for: {}", e);
1814 assert!(e.contains("back"), "Error must spell the set it would have taken: {}", e);
1815 }
1816 Ok(())
1817 }
1818
1819 #[test]
1820 fn test_reserved_icon_with_fallback_still_rejected_44() -> Outcome<()> {
1821 // The §4.5 hole, for the icon as for the surface: a reader that knows what code 16 is refuses
1822 // it in a document whether or not it is offered a stand-in.
1823 let icon = reserved(ReservedKind::Icon, mapdat!{
1824 "name" => dat!("home"),
1825 "fallback" => Dat::List(vec![
1826 Dat::Usr(
1827 UsrKindId::new(NodeKind::Para.code(), Some("para"), None),
1828 Some(Box::new(mapdat!{})),
1829 ),
1830 ]),
1831 });
1832 let tree = doc(vec![node(NodeKind::Para, mapdat!{ "children" => Dat::List(vec![icon]) })]);
1833 let e = match validate(&tree, SCHEMA_DOC) {
1834 Ok(_) => return Err(err!("An icon with a fallback was accepted in a document."; Test)),
1835 Err(e) => fmt!("{}", e),
1836 };
1837 assert!(e.contains("icon"), "Error must name the kind: {}", e);
1838 Ok(())
1839 }
1840
1841 #[test]
1842 fn test_reserved_surface_as_root_rejected_34() -> Outcome<()> {
1843 let root = reserved(ReservedKind::Surface, mapdat!{
1844 "app" => dat!("app.modeller"),
1845 });
1846 let e = match validate(&root, SCHEMA_DOC) {
1847 Ok(_) => return Err(err!("A surface as the root of a document was accepted."; Test)),
1848 Err(e) => fmt!("{}", e),
1849 };
1850 assert!(e.contains("Node 0"), "Error must name node 0: {}", e);
1851 assert!(e.contains("may not carry a surface node"),
1852 "Error must say a document may not carry it: {}", e);
1853 Ok(())
1854 }
1855}