1use super::dom::DomOp;
38
39pub const OPCODES: &[&str] = &[
41 "BeginPaint",
44 "CreateElement",
45 "CreateTextNode",
46 "SetText",
47 "SetHtml",
48 "ClearChildren",
49 "SetAttribute",
50 "AppendChild",
51 "SetValue",
52 "SetChecked",
53 "AddEventListener",
54 "SetStream",
55 "PreventDefault",
59 "MoveChildren",
62 "RemoveNode",
67 "InsertBefore",
68 "RemoveAttribute",
69 "Adopt",
70 "AdoptText",
71 "Listen",
72 "Unlisten",
73 "FreeNode",
74 "Hydrate",
77];
78
79const OP_BEGIN_PAINT: u8 = 0;
80
81pub const RETIRED_OPCODES: &[(u8, &str)] = &[
85 (
86 OP_BEGIN_PAINT,
87 "a paint's boundary is the commit that emits the stream, and no build since the retained tree writes it",
88 ),
89 (
90 OP_MOVE_CHILDREN,
91 "a slot's children are placed by the commit that emits the stream, and no build since the retained tree writes it",
92 ),
93];
94const OP_CREATE_ELEMENT: u8 = 1;
95const OP_CREATE_TEXT_NODE: u8 = 2;
96const OP_SET_TEXT: u8 = 3;
97const OP_SET_HTML: u8 = 4;
98const OP_CLEAR_CHILDREN: u8 = 5;
99const OP_SET_ATTRIBUTE: u8 = 6;
100const OP_APPEND_CHILD: u8 = 7;
101const OP_SET_VALUE: u8 = 8;
102const OP_SET_CHECKED: u8 = 9;
103const OP_ADD_EVENT_LISTENER: u8 = 10;
104const OP_SET_STREAM: u8 = 11;
105const OP_PREVENT_DEFAULT: u8 = 12;
106const OP_MOVE_CHILDREN: u8 = 13;
107const OP_REMOVE_NODE: u8 = 14;
108const OP_INSERT_BEFORE: u8 = 15;
109const OP_REMOVE_ATTRIBUTE: u8 = 16;
110const OP_ADOPT: u8 = 17;
111const OP_ADOPT_TEXT: u8 = 18;
112const OP_LISTEN: u8 = 19;
113const OP_UNLISTEN: u8 = 20;
114const OP_FREE_NODE: u8 = 21;
115const OP_HYDRATE: u8 = 22;
116
117fn put_u32(out: &mut Vec<u8>, v: u32) {
118 out.extend_from_slice(&v.to_le_bytes());
119}
120
121fn put_text(out: &mut Vec<u8>, s: &str) {
122 put_u32(out, s.len() as u32);
123 out.extend_from_slice(s.as_bytes());
124}
125
126pub fn encode(ops: &[DomOp]) -> Vec<u8> {
131 let mut out = Vec::new();
132 for op in ops {
133 match op {
134 DomOp::CreateElement { node, tag } => {
135 out.push(OP_CREATE_ELEMENT);
136 put_u32(&mut out, *node);
137 put_text(&mut out, tag);
138 }
139 DomOp::CreateTextNode { node, text } => {
140 out.push(OP_CREATE_TEXT_NODE);
141 put_u32(&mut out, *node);
142 put_text(&mut out, text);
143 }
144 DomOp::SetText { node, text } => {
145 out.push(OP_SET_TEXT);
146 put_u32(&mut out, *node);
147 put_text(&mut out, text);
148 }
149 DomOp::SetHtml { node, html } => {
150 out.push(OP_SET_HTML);
151 put_u32(&mut out, *node);
152 put_text(&mut out, html);
153 }
154 DomOp::ClearChildren { node } => {
155 out.push(OP_CLEAR_CHILDREN);
156 put_u32(&mut out, *node);
157 }
158 DomOp::SetAttribute { node, name, value } => {
159 out.push(OP_SET_ATTRIBUTE);
160 put_u32(&mut out, *node);
161 put_text(&mut out, name);
162 put_text(&mut out, value);
163 }
164 DomOp::AppendChild { parent, child } => {
165 out.push(OP_APPEND_CHILD);
166 put_u32(&mut out, *parent);
167 put_u32(&mut out, *child);
168 }
169 DomOp::SetValue { node, value } => {
170 out.push(OP_SET_VALUE);
171 put_u32(&mut out, *node);
172 put_text(&mut out, value);
173 }
174 DomOp::SetChecked { node, checked } => {
175 out.push(OP_SET_CHECKED);
176 put_u32(&mut out, *node);
177 out.push(u8::from(*checked));
178 }
179 DomOp::AddEventListener { node, event } => {
180 out.push(OP_ADD_EVENT_LISTENER);
181 put_u32(&mut out, *node);
182 put_text(&mut out, event);
183 }
184 DomOp::SetStream { node, stream } => {
185 out.push(OP_SET_STREAM);
186 put_u32(&mut out, *node);
187 out.extend_from_slice(&stream.to_le_bytes());
188 }
189 DomOp::PreventDefault { node, event } => {
190 out.push(OP_PREVENT_DEFAULT);
191 put_u32(&mut out, *node);
192 put_text(&mut out, event);
193 }
194 DomOp::RemoveNode { node } => {
195 out.push(OP_REMOVE_NODE);
196 put_u32(&mut out, *node);
197 }
198 DomOp::InsertBefore {
199 parent,
200 child,
201 before,
202 } => {
203 out.push(OP_INSERT_BEFORE);
204 put_u32(&mut out, *parent);
205 put_u32(&mut out, *child);
206 put_u32(&mut out, *before);
207 }
208 DomOp::RemoveAttribute { node, name } => {
209 out.push(OP_REMOVE_ATTRIBUTE);
210 put_u32(&mut out, *node);
211 put_text(&mut out, name);
212 }
213 DomOp::Adopt { parent, node, tag } => {
214 out.push(OP_ADOPT);
215 put_u32(&mut out, *parent);
216 put_u32(&mut out, *node);
217 put_text(&mut out, tag);
218 }
219 DomOp::AdoptText { parent, node, text } => {
220 out.push(OP_ADOPT_TEXT);
221 put_u32(&mut out, *parent);
222 put_u32(&mut out, *node);
223 put_text(&mut out, text);
224 }
225 DomOp::Hydrate { root } => {
226 out.push(OP_HYDRATE);
227 put_u32(&mut out, *root);
228 }
229 DomOp::Listen { node, event, flags } => {
230 out.push(OP_LISTEN);
231 put_u32(&mut out, *node);
232 put_text(&mut out, event);
233 out.push(*flags);
234 }
235 DomOp::Unlisten { node, event } => {
236 out.push(OP_UNLISTEN);
237 put_u32(&mut out, *node);
238 put_text(&mut out, event);
239 }
240 DomOp::FreeNode { node } => {
241 out.push(OP_FREE_NODE);
242 put_u32(&mut out, *node);
243 }
244 DomOp::RtcPeerConnection { .. }
249 | DomOp::RtcDataChannel { .. }
250 | DomOp::RtcCreateOffer { .. }
251 | DomOp::RtcCreateAnswer { .. }
252 | DomOp::RtcSetLocal { .. }
253 | DomOp::RtcSetRemote { .. }
254 | DomOp::RtcAddIce { .. }
255 | DomOp::RtcSend { .. } => {}
256 }
257 }
258 out
259}
260
261pub fn bytes_from_value(value: &crate::interpreter::RuntimeValue) -> Result<Vec<u8>, String> {
267 use crate::interpreter::RuntimeValue;
268 let RuntimeValue::List(list) = value else {
269 return Err(format!(
270 "`applyPatches` wants a patch buffer — a Seq of Int, each element one byte — and got \
271 {}.",
272 value.type_name()
273 ));
274 };
275 let mut out = Vec::new();
276 for (i, item) in list.borrow().to_values().into_iter().enumerate() {
277 let RuntimeValue::Int(n) = item else {
278 return Err(format!(
279 "a patch buffer holds bytes; element {} is {}.",
280 i + 1,
281 item.type_name()
282 ));
283 };
284 let byte = u8::try_from(n).map_err(|_| {
285 format!(
286 "a patch buffer holds bytes (0 to 255); element {} is {n}.",
287 i + 1
288 )
289 })?;
290 out.push(byte);
291 }
292 Ok(out)
293}
294
295pub fn apply(ops: &[DomOp]) -> Result<i64, String> {
316 super::dom::atomically(|| {
317 ops.iter().try_for_each(apply_one)?;
318 super::dom::end_hydration()
319 })
320}
321
322fn apply_one(op: &DomOp) -> Result<(), String> {
323 match op {
324 DomOp::CreateElement { node, tag } => {
325 return super::dom::create_element_as(*node as i64, tag)
326 }
327 DomOp::CreateTextNode { node, text } => {
328 return super::dom::create_text_node_as(*node as i64, text)
329 }
330 DomOp::SetText { node, text } => super::dom::set_text(*node as i64, text),
331 DomOp::SetHtml { node, html } => super::dom::set_html(*node as i64, html),
332 DomOp::ClearChildren { node } => super::dom::clear_children(*node as i64),
333 DomOp::SetAttribute { node, name, value } => {
334 super::dom::set_attribute(*node as i64, name, value)
335 }
336 DomOp::AppendChild { parent, child } => {
337 super::dom::append_child(*parent as i64, *child as i64)
338 }
339 DomOp::SetValue { node, value } => super::dom::set_value(*node as i64, value),
340 DomOp::SetChecked { node, checked } => super::dom::set_checked(*node as i64, *checked),
341 DomOp::AddEventListener { node, event } => {
345 super::dom::note_event_listener(*node as i64, event)
346 }
347 DomOp::PreventDefault { node, event } => super::dom::prevent_default(*node as i64, event),
348 DomOp::SetStream { node, stream } => super::dom::set_stream(*node as i64, *stream),
349 DomOp::RemoveNode { node } => super::dom::remove_node(*node as i64),
350 DomOp::InsertBefore {
351 parent,
352 child,
353 before,
354 } => super::dom::insert_before(*parent as i64, *child as i64, *before as i64),
355 DomOp::RemoveAttribute { node, name } => super::dom::remove_attribute(*node as i64, name),
356 DomOp::Hydrate { root } => return super::dom::hydrate(*root as i64),
357 DomOp::Adopt { parent, node, tag } => {
358 return super::dom::adopt(*parent as i64, *node as i64, tag)
359 }
360 DomOp::AdoptText { parent, node, text } => {
361 return super::dom::adopt_text(*parent as i64, *node as i64, text)
362 }
363 DomOp::Listen { node, event, flags } => super::dom::listen(*node as i64, event, *flags),
364 DomOp::Unlisten { node, event } => super::dom::unlisten(*node as i64, event),
365 DomOp::FreeNode { node } => super::dom::free_node(*node as i64),
366 DomOp::RtcPeerConnection { .. }
367 | DomOp::RtcDataChannel { .. }
368 | DomOp::RtcCreateOffer { .. }
369 | DomOp::RtcCreateAnswer { .. }
370 | DomOp::RtcSetLocal { .. }
371 | DomOp::RtcSetRemote { .. }
372 | DomOp::RtcAddIce { .. }
373 | DomOp::RtcSend { .. } => {}
374 }
375 Ok(())
376}
377
378struct Reader<'a> {
380 bytes: &'a [u8],
381 at: usize,
382}
383
384impl<'a> Reader<'a> {
385 fn take(&mut self, n: usize) -> Result<&'a [u8], String> {
386 let end = self.at.checked_add(n).ok_or_else(|| {
387 format!(
388 "a patch record at byte {} claims a length that overflows",
389 self.at
390 )
391 })?;
392 if end > self.bytes.len() {
393 return Err(format!(
394 "a patch record at byte {} needs {n} more bytes and the stream has {}",
395 self.at,
396 self.bytes.len() - self.at
397 ));
398 }
399 let slice = &self.bytes[self.at..end];
400 self.at = end;
401 Ok(slice)
402 }
403
404 fn u8(&mut self) -> Result<u8, String> {
405 Ok(self.take(1)?[0])
406 }
407
408 fn u32(&mut self) -> Result<u32, String> {
409 let b = self.take(4)?;
410 Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
411 }
412
413 fn i64(&mut self) -> Result<i64, String> {
414 let b = self.take(8)?;
415 Ok(i64::from_le_bytes([
416 b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
417 ]))
418 }
419
420 fn text(&mut self) -> Result<String, String> {
421 let len = self.u32()? as usize;
422 let bytes = self.take(len)?;
423 std::str::from_utf8(bytes)
425 .map(str::to_string)
426 .map_err(|e| format!("a patch record carries text that is not UTF-8: {e}"))
427 }
428
429 fn flag(&mut self) -> Result<bool, String> {
430 match self.u8()? {
431 0 => Ok(false),
432 1 => Ok(true),
433 other => Err(format!("a patch flag must be 0 or 1, got {other}")),
434 }
435 }
436}
437
438fn retired(code: u8) -> Option<&'static str> {
440 RETIRED_OPCODES
441 .iter()
442 .find(|(c, _)| *c == code)
443 .map(|(_, why)| *why)
444}
445
446fn retired_opcode(code: u8, at: &str) -> String {
448 format!(
449 "patch opcode {code} ({}) at {at} is retired: {}.",
450 OPCODES[code as usize],
451 retired(code).unwrap_or_default()
452 )
453}
454
455pub fn decode(bytes: &[u8]) -> Result<Vec<DomOp>, String> {
457 let mut r = Reader { bytes, at: 0 };
458 let mut ops = Vec::new();
459 while r.at < bytes.len() {
460 let code = r.u8()?;
461 let op = match code {
462 code if retired(code).is_some() => {
463 return Err(retired_opcode(code, &format!("byte {}", r.at - 1)))
464 }
465 OP_CREATE_ELEMENT => DomOp::CreateElement {
466 node: r.u32()?,
467 tag: r.text()?,
468 },
469 OP_CREATE_TEXT_NODE => DomOp::CreateTextNode {
470 node: r.u32()?,
471 text: r.text()?,
472 },
473 OP_SET_TEXT => DomOp::SetText {
474 node: r.u32()?,
475 text: r.text()?,
476 },
477 OP_SET_HTML => DomOp::SetHtml {
478 node: r.u32()?,
479 html: r.text()?,
480 },
481 OP_CLEAR_CHILDREN => DomOp::ClearChildren { node: r.u32()? },
482 OP_SET_ATTRIBUTE => DomOp::SetAttribute {
483 node: r.u32()?,
484 name: r.text()?,
485 value: r.text()?,
486 },
487 OP_APPEND_CHILD => DomOp::AppendChild {
488 parent: r.u32()?,
489 child: r.u32()?,
490 },
491 OP_SET_VALUE => DomOp::SetValue {
492 node: r.u32()?,
493 value: r.text()?,
494 },
495 OP_SET_CHECKED => DomOp::SetChecked {
496 node: r.u32()?,
497 checked: r.flag()?,
498 },
499 OP_ADD_EVENT_LISTENER => DomOp::AddEventListener {
500 node: r.u32()?,
501 event: r.text()?,
502 },
503 OP_SET_STREAM => DomOp::SetStream {
504 node: r.u32()?,
505 stream: r.i64()?,
506 },
507 OP_PREVENT_DEFAULT => DomOp::PreventDefault {
508 node: r.u32()?,
509 event: r.text()?,
510 },
511 OP_REMOVE_NODE => DomOp::RemoveNode { node: r.u32()? },
512 OP_INSERT_BEFORE => DomOp::InsertBefore {
513 parent: r.u32()?,
514 child: r.u32()?,
515 before: r.u32()?,
516 },
517 OP_REMOVE_ATTRIBUTE => DomOp::RemoveAttribute {
518 node: r.u32()?,
519 name: r.text()?,
520 },
521 OP_ADOPT => DomOp::Adopt {
522 parent: r.u32()?,
523 node: r.u32()?,
524 tag: r.text()?,
525 },
526 OP_ADOPT_TEXT => DomOp::AdoptText {
527 parent: r.u32()?,
528 node: r.u32()?,
529 text: r.text()?,
530 },
531 OP_LISTEN => DomOp::Listen {
532 node: r.u32()?,
533 event: r.text()?,
534 flags: r.u8()?,
535 },
536 OP_UNLISTEN => DomOp::Unlisten {
537 node: r.u32()?,
538 event: r.text()?,
539 },
540 OP_FREE_NODE => DomOp::FreeNode { node: r.u32()? },
541 OP_HYDRATE => DomOp::Hydrate { root: r.u32()? },
542 other => {
543 return Err(format!(
544 "unknown patch opcode {other} (0x{other:02x}) at byte {}. The opcode table is \
545 append-only ({} operations today); a stream carrying a code this host does \
546 not know was emitted by a newer build.",
547 r.at - 1,
548 OPCODES.len()
549 ))
550 }
551 };
552 ops.push(op);
553 }
554 Ok(ops)
555}
556
557pub fn encode_words(ops: &[DomOp]) -> (Vec<i64>, Vec<String>) {
567 let mut words = Vec::new();
568 let mut strings: Vec<String> = Vec::new();
569 fn intern(strings: &mut Vec<String>, s: &str) -> i64 {
570 strings.push(s.to_string());
571 strings.len() as i64
572 }
573 for op in ops {
574 match op {
575 DomOp::CreateElement { node, tag } => {
576 words.push(OP_CREATE_ELEMENT as i64);
577 words.push(*node as i64);
578 words.push(intern(&mut strings, tag));
579 }
580 DomOp::CreateTextNode { node, text } => {
581 words.push(OP_CREATE_TEXT_NODE as i64);
582 words.push(*node as i64);
583 words.push(intern(&mut strings, text));
584 }
585 DomOp::SetText { node, text } => {
586 words.push(OP_SET_TEXT as i64);
587 words.push(*node as i64);
588 words.push(intern(&mut strings, text));
589 }
590 DomOp::SetHtml { node, html } => {
591 words.push(OP_SET_HTML as i64);
592 words.push(*node as i64);
593 words.push(intern(&mut strings, html));
594 }
595 DomOp::ClearChildren { node } => {
596 words.push(OP_CLEAR_CHILDREN as i64);
597 words.push(*node as i64);
598 }
599 DomOp::SetAttribute { node, name, value } => {
600 words.push(OP_SET_ATTRIBUTE as i64);
601 words.push(*node as i64);
602 words.push(intern(&mut strings, name));
603 words.push(intern(&mut strings, value));
604 }
605 DomOp::AppendChild { parent, child } => {
606 words.push(OP_APPEND_CHILD as i64);
607 words.push(*parent as i64);
608 words.push(*child as i64);
609 }
610 DomOp::SetValue { node, value } => {
611 words.push(OP_SET_VALUE as i64);
612 words.push(*node as i64);
613 words.push(intern(&mut strings, value));
614 }
615 DomOp::SetChecked { node, checked } => {
616 words.push(OP_SET_CHECKED as i64);
617 words.push(*node as i64);
618 words.push(i64::from(*checked));
619 }
620 DomOp::AddEventListener { node, event } => {
621 words.push(OP_ADD_EVENT_LISTENER as i64);
622 words.push(*node as i64);
623 words.push(intern(&mut strings, event));
624 }
625 DomOp::SetStream { node, stream } => {
626 words.push(OP_SET_STREAM as i64);
627 words.push(*node as i64);
628 words.push(*stream);
629 }
630 DomOp::PreventDefault { node, event } => {
631 words.push(OP_PREVENT_DEFAULT as i64);
632 words.push(*node as i64);
633 words.push(intern(&mut strings, event));
634 }
635 DomOp::RemoveNode { node } => {
636 words.push(OP_REMOVE_NODE as i64);
637 words.push(*node as i64);
638 }
639 DomOp::InsertBefore {
640 parent,
641 child,
642 before,
643 } => {
644 words.push(OP_INSERT_BEFORE as i64);
645 words.push(*parent as i64);
646 words.push(*child as i64);
647 words.push(*before as i64);
648 }
649 DomOp::RemoveAttribute { node, name } => {
650 words.push(OP_REMOVE_ATTRIBUTE as i64);
651 words.push(*node as i64);
652 words.push(intern(&mut strings, name));
653 }
654 DomOp::Adopt { parent, node, tag } => {
655 words.push(OP_ADOPT as i64);
656 words.push(*parent as i64);
657 words.push(*node as i64);
658 words.push(intern(&mut strings, tag));
659 }
660 DomOp::AdoptText { parent, node, text } => {
661 words.push(OP_ADOPT_TEXT as i64);
662 words.push(*parent as i64);
663 words.push(*node as i64);
664 words.push(intern(&mut strings, text));
665 }
666 DomOp::Hydrate { root } => {
667 words.push(OP_HYDRATE as i64);
668 words.push(*root as i64);
669 }
670 DomOp::Listen { node, event, flags } => {
671 words.push(OP_LISTEN as i64);
672 words.push(*node as i64);
673 words.push(intern(&mut strings, event));
674 words.push(*flags as i64);
675 }
676 DomOp::Unlisten { node, event } => {
677 words.push(OP_UNLISTEN as i64);
678 words.push(*node as i64);
679 words.push(intern(&mut strings, event));
680 }
681 DomOp::FreeNode { node } => {
682 words.push(OP_FREE_NODE as i64);
683 words.push(*node as i64);
684 }
685 DomOp::RtcPeerConnection { .. }
687 | DomOp::RtcDataChannel { .. }
688 | DomOp::RtcCreateOffer { .. }
689 | DomOp::RtcCreateAnswer { .. }
690 | DomOp::RtcSetLocal { .. }
691 | DomOp::RtcSetRemote { .. }
692 | DomOp::RtcAddIce { .. }
693 | DomOp::RtcSend { .. } => {}
694 }
695 }
696 (words, strings)
697}
698
699struct WordReader<'a> {
702 words: &'a [i64],
703 strings: &'a [String],
704 at: usize,
705}
706
707impl<'a> WordReader<'a> {
708 fn word(&mut self) -> Result<i64, String> {
709 let w = *self.words.get(self.at).ok_or_else(|| {
710 format!(
711 "a word patch record at word {} needs one more word and the stream has none",
712 self.at
713 )
714 })?;
715 self.at += 1;
716 Ok(w)
717 }
718
719 fn u32(&mut self) -> Result<u32, String> {
720 let w = self.word()?;
721 u32::try_from(w).map_err(|_| {
722 format!(
723 "a word patch record at word {} carries {w}, which does not fit a node id (u32)",
724 self.at - 1
725 )
726 })
727 }
728
729 fn text(&mut self) -> Result<String, String> {
730 let idx = self.word()?;
731 let i = usize::try_from(idx)
732 .ok()
733 .filter(|&i| i >= 1 && i <= self.strings.len())
734 .ok_or_else(|| {
735 format!(
736 "a word patch record's string index {idx} is out of range (1..={}) at word {}",
737 self.strings.len(),
738 self.at - 1
739 )
740 })?;
741 Ok(self.strings[i - 1].clone())
742 }
743
744 fn flag(&mut self) -> Result<bool, String> {
745 match self.word()? {
746 0 => Ok(false),
747 1 => Ok(true),
748 other => Err(format!(
749 "a word patch flag must be 0 or 1, got {other} at word {}",
750 self.at - 1
751 )),
752 }
753 }
754
755 fn u8(&mut self) -> Result<u8, String> {
756 let w = self.word()?;
757 u8::try_from(w).map_err(|_| {
758 format!(
759 "a word patch record at word {} carries {w}, which does not fit a flags byte",
760 self.at - 1
761 )
762 })
763 }
764}
765
766pub fn decode_words(words: &[i64], strings: &[String]) -> Result<Vec<DomOp>, String> {
769 let mut r = WordReader {
770 words,
771 strings,
772 at: 0,
773 };
774 let mut ops = Vec::new();
775 while r.at < words.len() {
776 let code = r.word()?;
777 let code = u8::try_from(code).map_err(|_| {
778 format!(
779 "a word patch opcode {code} at word {} does not fit a byte",
780 r.at - 1
781 )
782 })?;
783 let op = match code {
784 code if retired(code).is_some() => {
785 return Err(retired_opcode(code, &format!("word {}", r.at - 1)))
786 }
787 OP_CREATE_ELEMENT => DomOp::CreateElement {
788 node: r.u32()?,
789 tag: r.text()?,
790 },
791 OP_CREATE_TEXT_NODE => DomOp::CreateTextNode {
792 node: r.u32()?,
793 text: r.text()?,
794 },
795 OP_SET_TEXT => DomOp::SetText {
796 node: r.u32()?,
797 text: r.text()?,
798 },
799 OP_SET_HTML => DomOp::SetHtml {
800 node: r.u32()?,
801 html: r.text()?,
802 },
803 OP_CLEAR_CHILDREN => DomOp::ClearChildren { node: r.u32()? },
804 OP_SET_ATTRIBUTE => DomOp::SetAttribute {
805 node: r.u32()?,
806 name: r.text()?,
807 value: r.text()?,
808 },
809 OP_APPEND_CHILD => DomOp::AppendChild {
810 parent: r.u32()?,
811 child: r.u32()?,
812 },
813 OP_SET_VALUE => DomOp::SetValue {
814 node: r.u32()?,
815 value: r.text()?,
816 },
817 OP_SET_CHECKED => DomOp::SetChecked {
818 node: r.u32()?,
819 checked: r.flag()?,
820 },
821 OP_ADD_EVENT_LISTENER => DomOp::AddEventListener {
822 node: r.u32()?,
823 event: r.text()?,
824 },
825 OP_SET_STREAM => DomOp::SetStream {
826 node: r.u32()?,
827 stream: r.word()?,
828 },
829 OP_PREVENT_DEFAULT => DomOp::PreventDefault {
830 node: r.u32()?,
831 event: r.text()?,
832 },
833 OP_REMOVE_NODE => DomOp::RemoveNode { node: r.u32()? },
834 OP_INSERT_BEFORE => DomOp::InsertBefore {
835 parent: r.u32()?,
836 child: r.u32()?,
837 before: r.u32()?,
838 },
839 OP_REMOVE_ATTRIBUTE => DomOp::RemoveAttribute {
840 node: r.u32()?,
841 name: r.text()?,
842 },
843 OP_ADOPT => DomOp::Adopt {
844 parent: r.u32()?,
845 node: r.u32()?,
846 tag: r.text()?,
847 },
848 OP_ADOPT_TEXT => DomOp::AdoptText {
849 parent: r.u32()?,
850 node: r.u32()?,
851 text: r.text()?,
852 },
853 OP_LISTEN => DomOp::Listen {
854 node: r.u32()?,
855 event: r.text()?,
856 flags: r.u8()?,
857 },
858 OP_UNLISTEN => DomOp::Unlisten {
859 node: r.u32()?,
860 event: r.text()?,
861 },
862 OP_FREE_NODE => DomOp::FreeNode { node: r.u32()? },
863 OP_HYDRATE => DomOp::Hydrate { root: r.u32()? },
864 other => {
865 return Err(format!(
866 "unknown word patch opcode {other} at word {}. The opcode table is \
867 append-only ({} operations today); a stream carrying a code this host does \
868 not know was emitted by a newer build.",
869 r.at - 1,
870 OPCODES.len()
871 ))
872 }
873 };
874 ops.push(op);
875 }
876 Ok(ops)
877}
878
879fn prefix_of(
883 value: &crate::interpreter::RuntimeValue,
884 count: i64,
885 what: &str,
886) -> Result<Vec<crate::interpreter::RuntimeValue>, String> {
887 use crate::interpreter::RuntimeValue;
888 let RuntimeValue::List(list) = value else {
889 return Err(format!(
890 "a word patch stream's {what} is a sequence, and got {}.",
891 value.type_name()
892 ));
893 };
894 let items = list.borrow().to_values();
895 let n = usize::try_from(count)
896 .ok()
897 .filter(|&n| n <= items.len())
898 .ok_or_else(|| {
899 format!(
900 "a word patch stream names {count} {what}, and its sequence holds {}.",
901 items.len()
902 )
903 })?;
904 Ok(items.into_iter().take(n).collect())
905}
906
907pub fn words_from_value(
910 value: &crate::interpreter::RuntimeValue,
911 count: i64,
912) -> Result<Vec<i64>, String> {
913 use crate::interpreter::RuntimeValue;
914 prefix_of(value, count, "words")?
915 .into_iter()
916 .enumerate()
917 .map(|(i, item)| match item {
918 RuntimeValue::Int(n) => Ok(n),
919 other => Err(format!(
920 "a word patch stream holds Ints; word {} is {}.",
921 i + 1,
922 other.type_name()
923 )),
924 })
925 .collect()
926}
927
928pub fn strings_from_value(
930 value: &crate::interpreter::RuntimeValue,
931 count: i64,
932) -> Result<Vec<String>, String> {
933 use crate::interpreter::RuntimeValue;
934 prefix_of(value, count, "strings")?
935 .into_iter()
936 .enumerate()
937 .map(|(i, item)| match item {
938 RuntimeValue::Text(s) => Ok(s.as_str().to_string()),
939 other => Err(format!(
940 "a word patch stream's string table holds Text; string {} is {}.",
941 i + 1,
942 other.type_name()
943 )),
944 })
945 .collect()
946}