1use satteri_arena::{Arena, ArenaBuilder, ArenaKind, Hast, Mdast, StringRef};
24use satteri_ast::commands::CommandError;
25use satteri_ast::hast::HastNodeType;
26use satteri_ast::mdast::codec::*;
27use satteri_ast::mdast::MdastNodeType;
28use satteri_ast::rebuild::{Patch, REF_NODE_TYPE};
29#[cfg(feature = "mdx")]
30use satteri_ast::shared::{MDX_ATTR_BOOLEAN_PROP, MDX_ATTR_LITERAL_PROP, MDX_ATTR_SPREAD};
31use satteri_ast::shared::{
32 PROP_BOOL_FALSE, PROP_BOOL_TRUE, PROP_INT, PROP_NULL, PROP_SPACE_SEP, PROP_STRING,
33};
34
35use crate::generated::prop_slots::{mdast_prop_slot, MdastPropSlot};
36use crate::generated::wire_constants::*;
37
38struct BufReader<'a> {
39 data: &'a [u8],
40 pos: usize,
41}
42
43impl<'a> BufReader<'a> {
44 fn new(data: &'a [u8]) -> Self {
45 Self { data, pos: 0 }
46 }
47
48 fn remaining(&self) -> usize {
49 self.data.len() - self.pos
50 }
51
52 fn read_u8(&mut self) -> Result<u8, CommandError> {
53 if self.remaining() < 1 {
54 return Err(CommandError::UnexpectedEof);
55 }
56 let v = self.data[self.pos];
57 self.pos += 1;
58 Ok(v)
59 }
60
61 fn read_u32(&mut self) -> Result<u32, CommandError> {
62 if self.remaining() < 4 {
63 return Err(CommandError::UnexpectedEof);
64 }
65 let v = u32::from_le_bytes([
66 self.data[self.pos],
67 self.data[self.pos + 1],
68 self.data[self.pos + 2],
69 self.data[self.pos + 3],
70 ]);
71 self.pos += 4;
72 Ok(v)
73 }
74
75 fn read_bytes(&mut self, len: usize) -> Result<&'a [u8], CommandError> {
76 if self.remaining() < len {
77 return Err(CommandError::UnexpectedEof);
78 }
79 let slice = &self.data[self.pos..self.pos + len];
80 self.pos += len;
81 Ok(slice)
82 }
83
84 fn read_str(&mut self, len: usize) -> Result<&'a str, CommandError> {
85 let bytes = self.read_bytes(len)?;
86 std::str::from_utf8(bytes).map_err(|_| CommandError::InvalidUtf8)
87 }
88}
89
90fn apply_data_property<K: ArenaKind>(
93 arena: &mut Arena<K>,
94 node_id: u32,
95 value_type: u8,
96 value_str: &str,
97) {
98 if value_type == PROP_NULL {
99 arena.set_node_data(node_id, Vec::new());
100 } else {
101 arena.set_node_data(node_id, value_str.as_bytes().to_vec());
102 }
103}
104
105fn mdast_type_name(node_type: u8) -> String {
107 match MdastNodeType::from_u8(node_type) {
108 Some(t) => t.name().to_string(),
109 None => format!("unknown({node_type})"),
110 }
111}
112
113fn apply_mdast_set_property(
117 arena: &mut Arena<Mdast>,
118 node_id: u32,
119 prop_name: &str,
120 value_type: u8,
121 value_str: &str,
122) -> Result<(), CommandError> {
123 if node_id as usize >= arena.len() {
124 return Err(CommandError::InvalidNodeId(node_id));
125 }
126 if prop_name == "data" {
127 apply_data_property(arena, node_id, value_type, value_str);
128 return Ok(());
129 }
130
131 let node_type = arena.get_node(node_id).node_type;
132
133 let slot = mdast_prop_slot(node_type, prop_name).ok_or_else(|| CommandError::UnknownField {
135 node_type: mdast_type_name(node_type),
136 name: prop_name.to_string(),
137 })?;
138
139 let written = write_mdast_prop_slot(arena, node_id, slot, prop_name, value_type, value_str)?;
142 written.ok_or_else(|| CommandError::InvalidPropertyValue {
143 node_type: mdast_type_name(node_type),
144 name: prop_name.to_string(),
145 })
146}
147
148fn write_mdast_prop_slot(
152 arena: &mut Arena<Mdast>,
153 node_id: u32,
154 slot: MdastPropSlot,
155 prop_name: &str,
156 value_type: u8,
157 value_str: &str,
158) -> Result<Option<()>, CommandError> {
159 use MdastPropSlot as S;
160 match value_type {
161 PROP_STRING | PROP_SPACE_SEP => match slot {
162 S::Str { offset } => {
163 let sref = arena.alloc_string(value_str);
164 set_mdast_string_ref(arena, node_id, offset, sref)?;
165 }
166 S::Enum8 { offset, values } => {
167 let Some(v) = values.iter().position(|v| *v == value_str) else {
168 return Ok(None);
169 };
170 set_mdast_scalar(arena, node_id, offset, &[v as u8]);
171 }
172 _ => return Ok(None),
173 },
174 PROP_BOOL_TRUE | PROP_BOOL_FALSE => match slot {
175 S::Bool { offset } => {
176 let value = value_type == PROP_BOOL_TRUE;
177 set_mdast_scalar(arena, node_id, offset, &[value as u8]);
178 }
179 _ => return Ok(None),
180 },
181 PROP_INT => {
182 let parsed = value_str.parse::<i64>().ok().or_else(|| {
185 value_str
186 .parse::<f64>()
187 .ok()
188 .filter(|f| f.is_finite() && f.fract() == 0.0)
189 .map(|f| f as i64)
190 });
191 let node_type = arena.get_node(node_id).node_type;
192 let fitted = |max: u32| -> Result<u32, CommandError> {
193 match parsed {
194 Some(v) if (0..=max as i64).contains(&v) => Ok(v as u32),
195 _ => Err(CommandError::PropertyValueOutOfRange {
196 node_type: mdast_type_name(node_type),
197 name: prop_name.to_string(),
198 value: value_str.to_string(),
199 max,
200 }),
201 }
202 };
203 match slot {
204 S::U8 { offset } | S::CheckedTri { offset } => {
205 let value = fitted(u8::MAX as u32)?;
206 set_mdast_scalar(arena, node_id, offset, &[value as u8]);
207 }
208 S::U32 { offset } => {
209 let value = fitted(u32::MAX)?;
210 set_mdast_scalar(arena, node_id, offset, &value.to_le_bytes());
211 }
212 _ => return Ok(None),
213 }
214 }
215 PROP_NULL => match slot {
216 S::CheckedTri { offset } => set_mdast_scalar(arena, node_id, offset, &[2]),
218 S::Str { offset } => set_mdast_string_ref(arena, node_id, offset, StringRef::empty())?,
219 _ => return Ok(None),
220 },
221 _ => return Err(CommandError::InvalidPropertyValueType(value_type)),
222 }
223 Ok(Some(()))
224}
225
226fn set_mdast_string_ref(
228 arena: &mut Arena<Mdast>,
229 node_id: u32,
230 offset: usize,
231 sref: StringRef,
232) -> Result<(), CommandError> {
233 let node = arena.get_node(node_id);
234 let data_offset = node.data_offset as usize;
235 let data_len = node.data_len as usize;
236 if data_len < offset + 8 {
237 return Err(CommandError::TypeDataTooShort);
238 }
239 let abs_offset = data_offset + offset;
240 arena.type_data[abs_offset..abs_offset + 8].copy_from_slice(&sref.as_bytes());
241 Ok(())
242}
243
244fn set_mdast_scalar(arena: &mut Arena<Mdast>, node_id: u32, offset: usize, bytes: &[u8]) {
247 let node = arena.get_node(node_id);
248 let data_offset = node.data_offset as usize;
249 let data_len = node.data_len as usize;
250 if data_len >= offset + bytes.len() {
251 let abs_offset = data_offset + offset;
252 arena.type_data[abs_offset..abs_offset + bytes.len()].copy_from_slice(bytes);
253 }
254}
255
256fn escape_braces_in_html_text(html: &str) -> String {
264 let mut result = String::with_capacity(html.len());
265 let mut in_tag = false;
266 let mut in_quote: Option<char> = None;
267
268 for ch in html.chars() {
269 if in_tag {
270 match ch {
271 '"' | '\'' if in_quote == Some(ch) => {
272 in_quote = None;
273 result.push(ch);
274 }
275 '"' | '\'' if in_quote.is_none() => {
276 in_quote = Some(ch);
277 result.push(ch);
278 }
279 '>' if in_quote.is_none() => {
280 in_tag = false;
281 result.push(ch);
282 }
283 _ => result.push(ch),
284 }
285 } else {
286 match ch {
287 '<' => {
288 in_tag = true;
289 result.push(ch);
290 }
291 '{' => result.push_str("{'{'}"),
292 '}' => result.push_str("{'}'}"),
293 _ => result.push(ch),
294 }
295 }
296 }
297 result
298}
299
300#[derive(Debug, Clone, Copy)]
302pub struct MdastCommandOptions {
303 pub escape_raw_html_braces: bool,
309}
310
311impl Default for MdastCommandOptions {
312 fn default() -> Self {
313 Self {
314 escape_raw_html_braces: true,
315 }
316 }
317}
318
319fn emit_ref_node<K: ArenaKind>(ref_id: u32, builder: &mut ArenaBuilder<K>) {
323 builder.open_node_raw(REF_NODE_TYPE);
324 builder.set_data_current(&ref_id.to_le_bytes());
325 builder.close_node();
326}
327
328use crate::generated::encode::{
331 encode_hast_tail_from_ops, encode_mdast_tail_from_ops, encode_mdast_type_data_from_ops,
332 MAX_FIXED_TYPE_DATA,
333};
334
335pub(crate) fn alloc_opt_str<K: ArenaKind>(
336 builder: &mut ArenaBuilder<K>,
337 s: Option<&str>,
338) -> StringRef {
339 match s {
340 Some(v) if !v.is_empty() => builder.alloc_string(v),
341 _ => StringRef::empty(),
342 }
343}
344
345fn apply_hast_set_property(
351 arena: &mut Arena<Hast>,
352 node_id: u32,
353 prop_name: &str,
354 value_type: u8,
355 value_str: &str,
356) -> Result<(), CommandError> {
357 if node_id as usize >= arena.len() {
358 return Err(CommandError::InvalidNodeId(node_id));
359 }
360 if prop_name == "data" {
361 apply_data_property(arena, node_id, value_type, value_str);
362 return Ok(());
363 }
364
365 let raw_type = arena.get_node(node_id).node_type;
366 let node_type = HastNodeType::from_u8(raw_type)
367 .ok_or_else(|| CommandError::UnknownNodeType(format!("hast type 0x{raw_type:02x}")))?;
368
369 match node_type {
370 HastNodeType::Element => {
371 apply_hast_element_property(arena, node_id, prop_name, value_type, value_str)
372 }
373
374 HastNodeType::Text
375 | HastNodeType::Comment
376 | HastNodeType::Raw
377 | HastNodeType::MdxFlowExpression
378 | HastNodeType::MdxTextExpression
379 | HastNodeType::MdxEsm
380 if prop_name == "value" =>
381 {
382 let sref = arena.alloc_string(value_str);
383 let data = arena.get_type_data(node_id);
384 if data.len() >= 8 {
385 let data_offset = arena.get_node(node_id).data_offset as usize;
386 arena.type_data[data_offset..data_offset + 8].copy_from_slice(&sref.as_bytes());
387 Ok(())
388 } else {
389 Err(CommandError::TypeDataTooShort)
390 }
391 }
392
393 #[cfg(feature = "mdx")]
394 HastNodeType::MdxJsxElement | HastNodeType::MdxJsxTextElement => {
395 apply_hast_mdx_jsx_attribute(arena, node_id, prop_name, value_type, value_str)
396 }
397
398 _ => Err(CommandError::UnknownField {
399 node_type: node_type.name().to_string(),
400 name: prop_name.to_string(),
401 }),
402 }
403}
404
405#[cfg(feature = "mdx")]
419fn apply_hast_mdx_jsx_attribute(
420 arena: &mut Arena<Hast>,
421 node_id: u32,
422 attr_name: &str,
423 value_type: u8,
424 value_str: &str,
425) -> Result<(), CommandError> {
426 let old_data = arena.get_type_data(node_id).to_vec();
427 if old_data.len() < 16 {
428 return Err(CommandError::TypeDataTooShort);
429 }
430 let elem_name = decode_mdx_jsx_element_name(&old_data);
431 let explicit = decode_mdx_jsx_explicit(&old_data);
432 let attr_count = decode_mdx_jsx_attr_count(&old_data);
433
434 let (kind, val_ref) = match value_type {
436 PROP_BOOL_TRUE | PROP_NULL => (MDX_ATTR_BOOLEAN_PROP, StringRef::empty()),
437 PROP_BOOL_FALSE => (MDX_ATTR_LITERAL_PROP, arena.alloc_string("false")),
438 _ if value_str.is_empty() => (MDX_ATTR_LITERAL_PROP, StringRef::empty()),
439 _ => (MDX_ATTR_LITERAL_PROP, arena.alloc_string(value_str)),
440 };
441
442 let mut attrs: Vec<(u8, StringRef, StringRef)> = Vec::with_capacity(attr_count as usize + 1);
443 let mut name_ref: Option<StringRef> = None;
444 for i in 0..attr_count {
445 let (existing_kind, existing_name, existing_value) = decode_mdx_jsx_attr(&old_data, i);
446 if existing_kind != MDX_ATTR_SPREAD && arena.get_str(existing_name) == attr_name {
447 name_ref = Some(existing_name);
448 continue;
449 }
450 attrs.push((existing_kind, existing_name, existing_value));
451 }
452 let name_ref = name_ref.unwrap_or_else(|| arena.alloc_string(attr_name));
453 attrs.push((kind, name_ref, val_ref));
454
455 arena.set_type_data(
456 node_id,
457 &encode_mdx_jsx_element_data(elem_name, &attrs, explicit),
458 );
459 Ok(())
460}
461
462fn apply_hast_element_property(
464 arena: &mut Arena<Hast>,
465 node_id: u32,
466 prop_name: &str,
467 value_type: u8,
468 value_str: &str,
469) -> Result<(), CommandError> {
470 let old_data = arena.get_type_data(node_id).to_vec();
471 if old_data.len() < 16 {
472 return Err(CommandError::TypeDataTooShort);
473 }
474
475 let old_prop_count = u32::from_le_bytes(old_data[8..12].try_into().unwrap()) as usize;
476
477 let mut found_index: Option<usize> = None;
478 for i in 0..old_prop_count {
479 let base = 16 + i * 20;
480 let name_off = u32::from_le_bytes(old_data[base..base + 4].try_into().unwrap());
481 let name_len = u32::from_le_bytes(old_data[base + 4..base + 8].try_into().unwrap());
482 let existing_name = arena.get_str(StringRef::new(name_off, name_len));
483 if existing_name == prop_name {
484 found_index = Some(i);
485 break;
486 }
487 }
488
489 let name_ref = arena.alloc_string(prop_name);
490 let val_ref = if value_str.is_empty() {
491 StringRef::empty()
492 } else {
493 arena.alloc_string(value_str)
494 };
495
496 if let Some(idx) = found_index {
497 let mut new_data = old_data;
498 let base = 16 + idx * 20;
499 new_data[base..base + 4].copy_from_slice(&name_ref.offset.to_le_bytes());
500 new_data[base + 4..base + 8].copy_from_slice(&name_ref.len.to_le_bytes());
501 new_data[base + 8] = value_type;
502 new_data[base + 9..base + 12].copy_from_slice(&[0u8; 3]);
503 new_data[base + 12..base + 16].copy_from_slice(&val_ref.offset.to_le_bytes());
504 new_data[base + 16..base + 20].copy_from_slice(&val_ref.len.to_le_bytes());
505 arena.set_type_data(node_id, &new_data);
506 } else {
507 let new_prop_count = (old_prop_count + 1) as u32;
508 let mut new_data = Vec::with_capacity(16 + new_prop_count as usize * 20);
509 new_data.extend_from_slice(&old_data[0..8]);
510 new_data.extend_from_slice(&new_prop_count.to_le_bytes());
511 new_data.extend_from_slice(&0u32.to_le_bytes());
512 if old_prop_count > 0 {
513 new_data.extend_from_slice(&old_data[16..16 + old_prop_count * 20]);
514 }
515 new_data.extend_from_slice(&name_ref.offset.to_le_bytes());
516 new_data.extend_from_slice(&name_ref.len.to_le_bytes());
517 new_data.push(value_type);
518 new_data.extend_from_slice(&[0u8; 3]);
519 new_data.extend_from_slice(&val_ref.offset.to_le_bytes());
520 new_data.extend_from_slice(&val_ref.len.to_le_bytes());
521 arena.set_type_data(node_id, &new_data);
522 }
523
524 Ok(())
525}
526
527trait OpCollector<'a>: Sized {
538 type Kind: ArenaKind;
539 const NUMERIC_OPS: bool;
543
544 fn open(node_type: u8) -> Self;
545 fn check_tag(tag: u8) -> Result<(), CommandError>;
546 fn finalize(&mut self, builder: &mut ArenaBuilder<Self::Kind>);
547 fn str_field(&mut self, field: u8, value: &'a str);
548 fn bool_field(&mut self, field: u8, value: bool);
549 fn prop(&mut self, name: &'a str, kind: u8, value: &'a str);
550 fn data(&mut self, bytes: &'a [u8]);
551 fn u8_field(&mut self, _field: u8, _value: u8) {}
552 fn u32_field(&mut self, _field: u8, _value: u32) {}
553 fn align(&mut self, _bytes: &'a [u8]) {}
554}
555
556const MAX_OPSTREAM_DEPTH: usize = 128;
560
561fn replay_opstream<'a, C: OpCollector<'a>>(
564 ops: &'a [u8],
565 orig: &Arena<C::Kind>,
566 anchor: u32,
567) -> Result<Arena<C::Kind>, CommandError> {
568 let mut builder = ArenaBuilder::<C::Kind>::new(String::new());
569 let mut reader = BufReader::new(ops);
570 let mut stack: Vec<C> = Vec::new();
571
572 while reader.remaining() > 0 {
573 match reader.read_u8()? {
574 OP_OPEN => {
575 if let Some(c) = stack.last_mut() {
576 c.finalize(&mut builder);
577 }
578 let node_type = reader.read_u8()?;
579 C::check_tag(node_type)?;
580 if !stack.is_empty() && node_type == <C::Kind as ArenaKind>::ROOT_TAG {
584 return Err(CommandError::UnencodableNodeType("root"));
585 }
586 if stack.len() >= MAX_OPSTREAM_DEPTH {
587 return Err(CommandError::OpstreamTooDeep(MAX_OPSTREAM_DEPTH));
588 }
589 builder.open_node(node_type);
590 stack.push(C::open(node_type));
591 }
592 OP_CLOSE => {
593 let Some(mut c) = stack.pop() else {
594 return Err(CommandError::UnbalancedOpstream);
595 };
596 c.finalize(&mut builder);
597 builder.close_node();
598 }
599 OP_REF => {
600 if let Some(c) = stack.last_mut() {
601 c.finalize(&mut builder);
602 }
603 let id = reader.read_u32()?;
604 if id as usize >= orig.len() {
607 return Err(CommandError::InvalidNodeId(id));
608 }
609 emit_ref_node(id, &mut builder);
610 }
611 OP_KEEP_CHILDREN => {
612 if let Some(c) = stack.last_mut() {
613 c.finalize(&mut builder);
614 }
615 if anchor as usize >= orig.len() {
616 return Err(CommandError::InvalidNodeId(anchor));
617 }
618 for &child in orig.get_children(anchor) {
619 emit_ref_node(child, &mut builder);
620 }
621 }
622 OP_STR => {
623 let field = reader.read_u8()?;
624 let len = reader.read_u32()? as usize;
625 let value = reader.read_str(len)?;
626 if let Some(c) = stack.last_mut() {
627 c.str_field(field, value);
628 }
629 }
630 OP_U8 if C::NUMERIC_OPS => {
631 let field = reader.read_u8()?;
632 let value = reader.read_u8()?;
633 if let Some(c) = stack.last_mut() {
634 c.u8_field(field, value);
635 }
636 }
637 OP_U32 if C::NUMERIC_OPS => {
638 let field = reader.read_u8()?;
639 let value = reader.read_u32()?;
640 if let Some(c) = stack.last_mut() {
641 c.u32_field(field, value);
642 }
643 }
644 OP_BOOL => {
645 let field = reader.read_u8()?;
646 let value = reader.read_u8()? != 0;
647 if let Some(c) = stack.last_mut() {
648 c.bool_field(field, value);
649 }
650 }
651 OP_PROP => {
652 let name_len = reader.read_u32()? as usize;
653 let name = reader.read_str(name_len)?;
654 let kind = reader.read_u8()?;
655 let val_len = reader.read_u32()? as usize;
656 let value = reader.read_str(val_len)?;
657 if let Some(c) = stack.last_mut() {
658 c.prop(name, kind, value);
659 }
660 }
661 OP_ALIGN if C::NUMERIC_OPS => {
662 let len = reader.read_u32()? as usize;
663 let bytes = reader.read_bytes(len)?;
664 if let Some(c) = stack.last_mut() {
665 c.align(bytes);
666 }
667 }
668 OP_DATA => {
669 let len = reader.read_u32()? as usize;
670 let bytes = reader.read_bytes(len)?;
671 if let Some(c) = stack.last_mut() {
672 c.data(bytes);
673 }
674 }
675 other => return Err(CommandError::UnknownCommand(other)),
676 }
677 }
678
679 if !stack.is_empty() {
680 return Err(CommandError::UnbalancedOpstream);
681 }
682 Ok(builder.finish())
683}
684
685#[cfg(feature = "mdx")]
688pub(crate) fn intern_mdx_jsx_attrs<K: ArenaKind>(
689 props: &[(&str, u8, &str)],
690 builder: &mut ArenaBuilder<K>,
691) -> Vec<(u8, StringRef, StringRef)> {
692 let mut attrs = Vec::with_capacity(props.len());
693 for &(name, kind, value) in props {
694 let nr = if kind == MDX_ATTR_SPREAD {
695 StringRef::empty()
696 } else {
697 builder.alloc_string(name)
698 };
699 let vr = if kind == MDX_ATTR_BOOLEAN_PROP {
700 StringRef::empty()
701 } else {
702 builder.alloc_string(value)
703 };
704 attrs.push((kind, nr, vr));
705 }
706 attrs
707}
708
709#[derive(Default)]
712pub(crate) struct FieldCollector<'a> {
713 node_type: u8,
714 finalized: bool,
715 pub(crate) strs: [Option<&'a str>; OF_FIELD_COUNT],
716 pub(crate) depth: Option<u8>,
717 checked: Option<u8>,
718 start: Option<u32>,
719 ordered: Option<bool>,
720 spread: Option<bool>,
721 pub(crate) props: Vec<(&'a str, u8, &'a str)>,
723 pub(crate) align: Option<&'a [u8]>,
725 pub(crate) explicit: Option<bool>,
727 data: Option<&'a [u8]>,
728}
729
730fn finalize_collector(c: &mut FieldCollector<'_>, builder: &mut ArenaBuilder<Mdast>) {
732 const LIST: u8 = MdastNodeType::List as u8;
733 const LIST_ITEM: u8 = MdastNodeType::ListItem as u8;
734
735 if c.finalized {
736 return;
737 }
738 c.finalized = true;
739 let mut fixed = [0u8; MAX_FIXED_TYPE_DATA];
740 if let Some(len) = encode_mdast_type_data_from_ops(c, c.node_type, builder, &mut fixed) {
741 builder.set_data_current(&fixed[..len]);
742 } else if let Some(type_data) = encode_mdast_tail_from_ops(c, c.node_type, builder) {
743 builder.set_data_current(&type_data);
745 } else {
746 let type_data: Vec<u8> = match c.node_type {
747 LIST => encode_list_data(
748 c.ordered.unwrap_or(false),
749 c.start.unwrap_or(1),
750 c.spread.unwrap_or(false),
751 ),
752 LIST_ITEM => encode_list_item_data(c.checked.unwrap_or(2), c.spread.unwrap_or(false)),
754 _ => Vec::new(),
756 };
757 if !type_data.is_empty() {
758 builder.set_data_current(&type_data);
759 }
760 }
761 if let Some(data) = c.data {
762 let id = builder.current_node_id();
763 builder.arena_mut().set_node_data(id, data.to_vec());
764 }
765}
766
767impl<'a> OpCollector<'a> for FieldCollector<'a> {
768 type Kind = Mdast;
769 const NUMERIC_OPS: bool = true;
770
771 fn open(node_type: u8) -> Self {
772 FieldCollector {
773 node_type,
774 ..Default::default()
775 }
776 }
777
778 fn check_tag(tag: u8) -> Result<(), CommandError> {
781 let known = MdastNodeType::from_u8(tag).is_some();
782 #[cfg(not(feature = "mdx"))]
783 let known = known
784 && !matches!(
785 MdastNodeType::from_u8(tag),
786 Some(
787 MdastNodeType::MdxJsxFlowElement
788 | MdastNodeType::MdxJsxTextElement
789 | MdastNodeType::MdxFlowExpression
790 | MdastNodeType::MdxTextExpression
791 | MdastNodeType::MdxjsEsm
792 )
793 );
794 if known {
795 Ok(())
796 } else {
797 Err(CommandError::UnknownNodeType(format!(
798 "op-stream tag {tag}"
799 )))
800 }
801 }
802
803 fn finalize(&mut self, builder: &mut ArenaBuilder<Mdast>) {
804 finalize_collector(self, builder);
805 }
806
807 fn str_field(&mut self, field: u8, value: &'a str) {
808 let field = field as usize;
809 if field < self.strs.len() {
810 self.strs[field] = Some(value);
811 }
812 }
813
814 fn bool_field(&mut self, field: u8, value: bool) {
815 match field {
816 OF_ORDERED => self.ordered = Some(value),
817 OF_SPREAD => self.spread = Some(value),
818 OF_EXPLICIT => self.explicit = Some(value),
819 _ => {}
820 }
821 }
822
823 fn prop(&mut self, name: &'a str, kind: u8, value: &'a str) {
824 self.props.push((name, kind, value));
825 }
826
827 fn data(&mut self, bytes: &'a [u8]) {
828 self.data = Some(bytes);
829 }
830
831 fn u8_field(&mut self, field: u8, value: u8) {
832 match field {
833 OF_DEPTH => self.depth = Some(value),
834 OF_CHECKED => self.checked = Some(value),
835 _ => {}
836 }
837 }
838
839 fn u32_field(&mut self, field: u8, value: u32) {
840 if field == OF_START {
841 self.start = Some(value);
842 }
843 }
844
845 fn align(&mut self, bytes: &'a [u8]) {
846 self.align = Some(bytes);
847 }
848}
849
850fn replay_mdast_opstream(
851 ops: &[u8],
852 orig: &Arena<Mdast>,
853 anchor: u32,
854) -> Result<Arena<Mdast>, CommandError> {
855 replay_opstream::<FieldCollector>(ops, orig, anchor)
856}
857
858fn read_mdast_payload(
862 reader: &mut BufReader<'_>,
863 parse_markdown: &dyn Fn(&str) -> Arena<Mdast>,
864 orig: &Arena<Mdast>,
865 anchor: u32,
866 options: MdastCommandOptions,
867) -> Result<(Arena<Mdast>, bool), CommandError> {
868 let payload_type = reader.read_u8()?;
869 let len = reader.read_u32()? as usize;
870
871 match payload_type {
872 PAYLOAD_RAW_MARKDOWN => {
873 let md = reader.read_str(len)?;
874 Ok((parse_markdown(md), false))
875 }
876 PAYLOAD_RAW_HTML => {
877 let html = reader.read_str(len)?;
878 if options.escape_raw_html_braces {
879 let escaped = escape_braces_in_html_text(html);
880 Ok((parse_markdown(&escaped), false))
881 } else {
882 Ok((parse_markdown(html), false))
883 }
884 }
885 PAYLOAD_OPSTREAM => {
886 let ops = reader.read_bytes(len)?;
887 Ok((replay_mdast_opstream(ops, orig, anchor)?, false))
888 }
889 other => Err(CommandError::UnknownPayloadType(other)),
890 }
891}
892
893#[derive(Default)]
897pub(crate) struct HastFieldCollector<'a> {
898 node_type: u8,
899 finalized: bool,
900 pub(crate) tag: Option<&'a str>,
902 value: Option<&'a str>,
903 pub(crate) props: Vec<(&'a str, u8, &'a str)>,
904 pub(crate) explicit: Option<bool>,
906 data: Option<&'a [u8]>,
907}
908
909fn finalize_hast_collector(c: &mut HastFieldCollector<'_>, builder: &mut ArenaBuilder<Hast>) {
910 const TEXT: u8 = HastNodeType::Text as u8;
911 const COMMENT: u8 = HastNodeType::Comment as u8;
912 const RAW: u8 = HastNodeType::Raw as u8;
913 const MDX_FLOW_EXPRESSION: u8 = HastNodeType::MdxFlowExpression as u8;
914 const MDX_ESM: u8 = HastNodeType::MdxEsm as u8;
915 const MDX_TEXT_EXPRESSION: u8 = HastNodeType::MdxTextExpression as u8;
916
917 if c.finalized {
918 return;
919 }
920 c.finalized = true;
921 if let Some(type_data) = encode_hast_tail_from_ops(c, c.node_type, builder) {
922 builder.set_data_current(&type_data);
924 } else {
925 let type_data: Vec<u8> = match c.node_type {
926 TEXT | COMMENT | RAW | MDX_FLOW_EXPRESSION | MDX_ESM | MDX_TEXT_EXPRESSION => {
927 let sref = builder.alloc_string(c.value.unwrap_or(""));
928 encode_string_ref_data(sref)
929 }
930 _ => Vec::new(),
932 };
933 if !type_data.is_empty() {
934 builder.set_data_current(&type_data);
935 }
936 }
937 if let Some(data) = c.data {
938 let id = builder.current_node_id();
939 builder.arena_mut().set_node_data(id, data.to_vec());
940 }
941}
942
943impl<'a> OpCollector<'a> for HastFieldCollector<'a> {
944 type Kind = Hast;
945 const NUMERIC_OPS: bool = false;
946
947 fn open(node_type: u8) -> Self {
948 HastFieldCollector {
949 node_type,
950 ..Default::default()
951 }
952 }
953
954 fn check_tag(tag: u8) -> Result<(), CommandError> {
956 if HastNodeType::from_u8(tag) == Some(HastNodeType::Doctype) {
960 return Err(CommandError::UnencodableNodeType("doctype"));
961 }
962 let known = HastNodeType::from_u8(tag).is_some();
963 #[cfg(not(feature = "mdx"))]
964 let known = known
965 && !matches!(
966 HastNodeType::from_u8(tag),
967 Some(
968 HastNodeType::MdxJsxElement
969 | HastNodeType::MdxJsxTextElement
970 | HastNodeType::MdxFlowExpression
971 | HastNodeType::MdxEsm
972 | HastNodeType::MdxTextExpression
973 )
974 );
975 if known {
976 Ok(())
977 } else {
978 Err(CommandError::UnknownNodeType(format!(
979 "op-stream tag {tag}"
980 )))
981 }
982 }
983
984 fn finalize(&mut self, builder: &mut ArenaBuilder<Hast>) {
985 finalize_hast_collector(self, builder);
986 }
987
988 fn str_field(&mut self, field: u8, value: &'a str) {
989 match field {
990 OF_TAGNAME | OF_NAME => self.tag = Some(value),
991 OF_VALUE => self.value = Some(value),
992 _ => {}
993 }
994 }
995
996 fn bool_field(&mut self, field: u8, value: bool) {
997 if field == OF_EXPLICIT {
998 self.explicit = Some(value);
999 }
1000 }
1001
1002 fn prop(&mut self, name: &'a str, kind: u8, value: &'a str) {
1003 self.props.push((name, kind, value));
1004 }
1005
1006 fn data(&mut self, bytes: &'a [u8]) {
1007 self.data = Some(bytes);
1008 }
1009}
1010
1011fn replay_hast_opstream(
1012 ops: &[u8],
1013 orig: &Arena<Hast>,
1014 anchor: u32,
1015) -> Result<Arena<Hast>, CommandError> {
1016 replay_opstream::<HastFieldCollector>(ops, orig, anchor)
1017}
1018
1019fn read_hast_payload(
1023 reader: &mut BufReader<'_>,
1024 orig: &Arena<Hast>,
1025 anchor: u32,
1026) -> Result<(Arena<Hast>, bool), CommandError> {
1027 let payload_type = reader.read_u8()?;
1028 let len = reader.read_u32()? as usize;
1029
1030 match payload_type {
1031 PAYLOAD_OPSTREAM => {
1032 let ops = reader.read_bytes(len)?;
1033 Ok((replay_hast_opstream(ops, orig, anchor)?, false))
1034 }
1035 other => Err(CommandError::UnknownPayloadType(other)),
1036 }
1037}
1038
1039pub fn apply_mdast_commands(
1062 arena: Arena<Mdast>,
1063 command_buf: &[u8],
1064 parse_markdown: &dyn Fn(&str) -> Arena<Mdast>,
1065) -> Result<Arena<Mdast>, CommandError> {
1066 apply_mdast_commands_with_options(
1067 arena,
1068 command_buf,
1069 parse_markdown,
1070 MdastCommandOptions::default(),
1071 )
1072}
1073
1074pub fn apply_mdast_commands_with_options(
1076 arena: Arena<Mdast>,
1077 command_buf: &[u8],
1078 parse_markdown: &dyn Fn(&str) -> Arena<Mdast>,
1079 options: MdastCommandOptions,
1080) -> Result<Arena<Mdast>, CommandError> {
1081 let (arena, dropped) =
1082 apply_mdast_commands_lenient_with_options(arena, command_buf, parse_markdown, options)?;
1083 if let Some(anchor) = dropped.first() {
1084 return Err(CommandError::PatchOnRemovedSubtree(*anchor));
1085 }
1086 Ok(arena)
1087}
1088
1089pub fn apply_mdast_commands_lenient(
1096 arena: Arena<Mdast>,
1097 command_buf: &[u8],
1098 parse_markdown: &dyn Fn(&str) -> Arena<Mdast>,
1099) -> Result<(Arena<Mdast>, Vec<u32>), CommandError> {
1100 apply_mdast_commands_lenient_with_options(
1101 arena,
1102 command_buf,
1103 parse_markdown,
1104 MdastCommandOptions::default(),
1105 )
1106}
1107
1108pub fn apply_mdast_commands_lenient_with_options(
1111 mut arena: Arena<Mdast>,
1112 command_buf: &[u8],
1113 parse_markdown: &dyn Fn(&str) -> Arena<Mdast>,
1114 options: MdastCommandOptions,
1115) -> Result<(Arena<Mdast>, Vec<u32>), CommandError> {
1116 if command_buf.is_empty() {
1117 return Ok((arena, Vec::new()));
1118 }
1119
1120 let mut patches: Vec<Patch<Mdast>> = Vec::new();
1121 let mut reader = BufReader::new(command_buf);
1122
1123 while reader.remaining() > 0 {
1124 let cmd = reader.read_u8()?;
1125
1126 match cmd {
1127 CMD_REMOVE => {
1128 let node_id = reader.read_u32()?;
1129 patches.push(Patch::Remove { node_id });
1130 }
1131
1132 CMD_SET_PROPERTY => {
1133 let node_id = reader.read_u32()?;
1134 let value_type = reader.read_u8()?;
1135 let name_len = reader.read_u32()? as usize;
1136 let name = reader.read_str(name_len)?;
1137 let value_len = reader.read_u32()? as usize;
1138 let value = reader.read_str(value_len)?;
1139 apply_mdast_set_property(&mut arena, node_id, name, value_type, value)?;
1140 }
1141
1142 CMD_INSERT_BEFORE => {
1143 let node_id = reader.read_u32()?;
1144 let (new_tree, _) =
1145 read_mdast_payload(&mut reader, parse_markdown, &arena, node_id, options)?;
1146 patches.push(Patch::InsertBefore { node_id, new_tree });
1147 }
1148
1149 CMD_INSERT_AFTER => {
1150 let node_id = reader.read_u32()?;
1151 let (new_tree, _) =
1152 read_mdast_payload(&mut reader, parse_markdown, &arena, node_id, options)?;
1153 patches.push(Patch::InsertAfter { node_id, new_tree });
1154 }
1155
1156 CMD_PREPEND_CHILD => {
1157 let node_id = reader.read_u32()?;
1158 let (child_tree, _) =
1159 read_mdast_payload(&mut reader, parse_markdown, &arena, node_id, options)?;
1160 patches.push(Patch::PrependChild {
1161 node_id,
1162 child_tree,
1163 });
1164 }
1165
1166 CMD_APPEND_CHILD => {
1167 let node_id = reader.read_u32()?;
1168 let (child_tree, _) =
1169 read_mdast_payload(&mut reader, parse_markdown, &arena, node_id, options)?;
1170 patches.push(Patch::AppendChild {
1171 node_id,
1172 child_tree,
1173 });
1174 }
1175
1176 CMD_WRAP => {
1177 let node_id = reader.read_u32()?;
1178 let (parent_tree, _) =
1179 read_mdast_payload(&mut reader, parse_markdown, &arena, node_id, options)?;
1180 patches.push(Patch::Wrap {
1181 node_id,
1182 parent_tree,
1183 });
1184 }
1185
1186 CMD_REPLACE => {
1187 let node_id = reader.read_u32()?;
1188 let (new_tree, keep_children) =
1189 read_mdast_payload(&mut reader, parse_markdown, &arena, node_id, options)?;
1190 patches.push(Patch::Replace {
1191 node_id,
1192 new_tree,
1193 keep_children,
1194 });
1195 }
1196
1197 CMD_SET_CHILDREN => {
1198 let node_id = reader.read_u32()?;
1199 let (new_children, _) =
1200 read_mdast_payload(&mut reader, parse_markdown, &arena, node_id, options)?;
1201 patches.push(Patch::SetChildren {
1202 node_id,
1203 new_children,
1204 });
1205 }
1206
1207 other => return Err(CommandError::UnknownCommand(other)),
1208 }
1209 }
1210
1211 if patches.is_empty() {
1212 Ok((arena, Vec::new()))
1213 } else {
1214 let result = satteri_ast::rebuild::rebuild_lenient(&arena, &patches)?;
1215 Ok((result.arena, result.dropped))
1216 }
1217}
1218
1219pub fn apply_hast_commands(
1237 arena: Arena<Hast>,
1238 command_buf: &[u8],
1239) -> Result<Arena<Hast>, CommandError> {
1240 let (arena, dropped) = apply_hast_commands_lenient(arena, command_buf)?;
1241 if let Some(anchor) = dropped.first() {
1242 return Err(CommandError::PatchOnRemovedSubtree(*anchor));
1243 }
1244 Ok(arena)
1245}
1246
1247pub fn apply_hast_commands_lenient(
1253 mut arena: Arena<Hast>,
1254 command_buf: &[u8],
1255) -> Result<(Arena<Hast>, Vec<u32>), CommandError> {
1256 if command_buf.is_empty() {
1257 return Ok((arena, Vec::new()));
1258 }
1259
1260 let mut patches: Vec<Patch<Hast>> = Vec::new();
1261 let mut reader = BufReader::new(command_buf);
1262
1263 while reader.remaining() > 0 {
1264 let cmd = reader.read_u8()?;
1265
1266 match cmd {
1267 CMD_REMOVE => {
1268 let node_id = reader.read_u32()?;
1269 patches.push(Patch::Remove { node_id });
1270 }
1271
1272 CMD_SET_PROPERTY => {
1273 let node_id = reader.read_u32()?;
1274 let value_type = reader.read_u8()?;
1275 let name_len = reader.read_u32()? as usize;
1276 let name = reader.read_str(name_len)?;
1277 let value_len = reader.read_u32()? as usize;
1278 let value = reader.read_str(value_len)?;
1279 apply_hast_set_property(&mut arena, node_id, name, value_type, value)?;
1280 }
1281
1282 CMD_INSERT_BEFORE => {
1283 let node_id = reader.read_u32()?;
1284 let (new_tree, _) = read_hast_payload(&mut reader, &arena, node_id)?;
1285 patches.push(Patch::InsertBefore { node_id, new_tree });
1286 }
1287
1288 CMD_INSERT_AFTER => {
1289 let node_id = reader.read_u32()?;
1290 let (new_tree, _) = read_hast_payload(&mut reader, &arena, node_id)?;
1291 patches.push(Patch::InsertAfter { node_id, new_tree });
1292 }
1293
1294 CMD_PREPEND_CHILD => {
1295 let node_id = reader.read_u32()?;
1296 let (child_tree, _) = read_hast_payload(&mut reader, &arena, node_id)?;
1297 patches.push(Patch::PrependChild {
1298 node_id,
1299 child_tree,
1300 });
1301 }
1302
1303 CMD_APPEND_CHILD => {
1304 let node_id = reader.read_u32()?;
1305 let (child_tree, _) = read_hast_payload(&mut reader, &arena, node_id)?;
1306 patches.push(Patch::AppendChild {
1307 node_id,
1308 child_tree,
1309 });
1310 }
1311
1312 CMD_WRAP => {
1313 let node_id = reader.read_u32()?;
1314 let (parent_tree, _) = read_hast_payload(&mut reader, &arena, node_id)?;
1315 patches.push(Patch::Wrap {
1316 node_id,
1317 parent_tree,
1318 });
1319 }
1320
1321 CMD_REPLACE => {
1322 let node_id = reader.read_u32()?;
1323 let (new_tree, keep_children) = read_hast_payload(&mut reader, &arena, node_id)?;
1324 patches.push(Patch::Replace {
1325 node_id,
1326 new_tree,
1327 keep_children,
1328 });
1329 }
1330
1331 CMD_SET_CHILDREN => {
1332 let node_id = reader.read_u32()?;
1333 let (new_children, _) = read_hast_payload(&mut reader, &arena, node_id)?;
1334 patches.push(Patch::SetChildren {
1335 node_id,
1336 new_children,
1337 });
1338 }
1339
1340 other => return Err(CommandError::UnknownCommand(other)),
1341 }
1342 }
1343
1344 if patches.is_empty() {
1345 Ok((arena, Vec::new()))
1346 } else {
1347 let result = satteri_ast::rebuild::rebuild_lenient(&arena, &patches)?;
1348 Ok((result.arena, result.dropped))
1349 }
1350}
1351
1352#[cfg(test)]
1353mod tests {
1354 use super::*;
1355 use satteri_ast::shared::PROP_INT;
1356
1357 fn op_open(b: &mut Vec<u8>, t: MdastNodeType) {
1358 b.push(OP_OPEN);
1359 b.push(t as u8);
1360 }
1361 fn op_close(b: &mut Vec<u8>) {
1362 b.push(OP_CLOSE);
1363 }
1364 fn op_str(b: &mut Vec<u8>, field: u8, s: &str) {
1365 b.push(OP_STR);
1366 b.push(field);
1367 b.extend_from_slice(&(s.len() as u32).to_le_bytes());
1368 b.extend_from_slice(s.as_bytes());
1369 }
1370 fn op_u8(b: &mut Vec<u8>, field: u8, v: u8) {
1371 b.push(OP_U8);
1372 b.push(field);
1373 b.push(v);
1374 }
1375
1376 #[test]
1377 fn opstream_replay_builds_subtree() {
1378 let mut ops = Vec::new();
1380 op_open(&mut ops, MdastNodeType::Blockquote);
1381 op_open(&mut ops, MdastNodeType::Heading);
1382 op_u8(&mut ops, OF_DEPTH, 3);
1383 op_open(&mut ops, MdastNodeType::Text);
1384 op_str(&mut ops, OF_VALUE, "Note");
1385 op_close(&mut ops);
1386 op_close(&mut ops);
1387 op_open(&mut ops, MdastNodeType::Paragraph);
1388 op_open(&mut ops, MdastNodeType::Text);
1389 op_str(&mut ops, OF_VALUE, "Body");
1390 op_close(&mut ops);
1391 op_close(&mut ops);
1392 op_close(&mut ops);
1393
1394 let empty = ArenaBuilder::<Mdast>::new(String::new()).finish();
1395 let arena = replay_mdast_opstream(&ops, &empty, 0).unwrap();
1396
1397 assert_eq!(arena.get_node(0).node_type, MdastNodeType::Blockquote as u8);
1399 let top = arena.get_children(0).to_vec();
1400 assert_eq!(top.len(), 2);
1401 let h = top[0];
1403 assert_eq!(arena.get_node(h).node_type, MdastNodeType::Heading as u8);
1404 assert_eq!(decode_heading_data(arena.get_type_data(h)).depth, 3);
1405 let h_text = arena.get_children(h)[0];
1406 assert_eq!(arena.get_node(h_text).node_type, MdastNodeType::Text as u8);
1407 let sref = decode_string_ref_data(arena.get_type_data(h_text));
1408 assert_eq!(arena.get_str(sref), "Note");
1409 let p = top[1];
1411 assert_eq!(arena.get_node(p).node_type, MdastNodeType::Paragraph as u8);
1412 let p_text = arena.get_children(p)[0];
1413 assert_eq!(
1414 arena.get_str(decode_string_ref_data(arena.get_type_data(p_text))),
1415 "Body"
1416 );
1417 }
1418
1419 #[test]
1420 fn opstream_replay_rejects_unbalanced_close() {
1421 let empty = ArenaBuilder::<Mdast>::new(String::new()).finish();
1422 let err = replay_mdast_opstream(&[OP_CLOSE], &empty, 0).unwrap_err();
1423 assert!(matches!(err, CommandError::UnbalancedOpstream));
1424
1425 let empty_hast = ArenaBuilder::<Hast>::new(String::new()).finish();
1426 let err = replay_hast_opstream(&[OP_CLOSE], &empty_hast, 0).unwrap_err();
1427 assert!(matches!(err, CommandError::UnbalancedOpstream));
1428
1429 let mut ops = Vec::new();
1431 op_open(&mut ops, MdastNodeType::Paragraph);
1432 op_close(&mut ops);
1433 op_close(&mut ops);
1434 let err = replay_mdast_opstream(&ops, &empty, 0).unwrap_err();
1435 assert!(matches!(err, CommandError::UnbalancedOpstream));
1436 }
1437
1438 #[test]
1439 fn opstream_replay_rejects_unclosed_node() {
1440 let mut ops = Vec::new();
1443 op_open(&mut ops, MdastNodeType::Heading);
1444 op_u8(&mut ops, OF_DEPTH, 2);
1445
1446 let empty = ArenaBuilder::<Mdast>::new(String::new()).finish();
1447 let err = replay_mdast_opstream(&ops, &empty, 0).unwrap_err();
1448 assert!(matches!(err, CommandError::UnbalancedOpstream));
1449
1450 let hast_ops = vec![OP_OPEN, HastNodeType::Element as u8];
1451 let empty_hast = ArenaBuilder::<Hast>::new(String::new()).finish();
1452 let err = replay_hast_opstream(&hast_ops, &empty_hast, 0).unwrap_err();
1453 assert!(matches!(err, CommandError::UnbalancedOpstream));
1454 }
1455
1456 #[test]
1457 fn opstream_keep_children_rejects_out_of_range_anchor() {
1458 let orig = test_parse_markdown("Hello");
1459 let bad_anchor = orig.len() as u32;
1460
1461 let mut ops = Vec::new();
1462 op_open(&mut ops, MdastNodeType::Heading);
1463 ops.push(OP_KEEP_CHILDREN);
1464 op_close(&mut ops);
1465
1466 let err = replay_mdast_opstream(&ops, &orig, bad_anchor).unwrap_err();
1467 assert!(matches!(err, CommandError::InvalidNodeId(id) if id == bad_anchor));
1468 }
1469
1470 #[test]
1471 fn set_property_rejects_out_of_range_node_id() {
1472 let arena = build_hello_world();
1473 let bad_id = arena.len() as u32;
1474 let mut buf = Vec::new();
1475 push_set_property(&mut buf, bad_id, PROP_INT, "depth", "3");
1476 let err = apply_mdast_commands(arena, &buf, &test_parse_markdown).unwrap_err();
1477 assert!(matches!(err, CommandError::InvalidNodeId(id) if id == bad_id));
1478
1479 let hast = build_hast_element(&[]);
1480 let bad_id = hast.len() as u32;
1481 let mut buf = Vec::new();
1482 push_set_property(&mut buf, bad_id, PROP_STRING, "class", "x");
1483 let err = apply_hast_commands(hast, &buf).unwrap_err();
1484 assert!(matches!(err, CommandError::InvalidNodeId(id) if id == bad_id));
1485 }
1486
1487 #[test]
1488 fn opstream_replay_rejects_unknown_tags() {
1489 let empty = ArenaBuilder::<Mdast>::new(String::new()).finish();
1490 let err = replay_mdast_opstream(&[OP_OPEN, 200], &empty, 0).unwrap_err();
1491 assert!(matches!(err, CommandError::UnknownNodeType(_)));
1492
1493 let empty_hast = ArenaBuilder::<Hast>::new(String::new()).finish();
1494 let err = replay_hast_opstream(&[OP_OPEN, 200], &empty_hast, 0).unwrap_err();
1495 assert!(matches!(err, CommandError::UnknownNodeType(_)));
1496 }
1497
1498 #[test]
1499 fn opstream_keep_children_splices_original_children() {
1500 let orig = test_parse_markdown("Hello");
1503 let para = orig.get_children(0)[0];
1504 let orig_text = orig.get_children(para)[0];
1505
1506 let mut ops = Vec::new();
1507 op_open(&mut ops, MdastNodeType::Heading);
1508 op_u8(&mut ops, OF_DEPTH, 2);
1509 ops.push(OP_KEEP_CHILDREN);
1510 op_close(&mut ops);
1511
1512 let arena = replay_mdast_opstream(&ops, &orig, para).unwrap();
1513 assert_eq!(arena.get_node(0).node_type, MdastNodeType::Heading as u8);
1514 assert_eq!(decode_heading_data(arena.get_type_data(0)).depth, 2);
1515 let children = arena.get_children(0).to_vec();
1516 assert_eq!(children.len(), 1);
1517 assert_eq!(arena.get_node(children[0]).node_type, REF_NODE_TYPE);
1518 assert_eq!(
1519 u32::from_le_bytes(arena.get_type_data(children[0]).try_into().unwrap()),
1520 orig_text
1521 );
1522 }
1523
1524 #[test]
1525 fn opstream_ref_rejects_out_of_range_id() {
1526 let orig = test_parse_markdown("Hello");
1529 let bad = orig.len() as u32 + 100;
1530
1531 let mut ops = Vec::new();
1532 op_open(&mut ops, MdastNodeType::Paragraph);
1533 ops.push(OP_REF);
1534 ops.extend_from_slice(&bad.to_le_bytes());
1535 op_close(&mut ops);
1536
1537 let err = replay_mdast_opstream(&ops, &orig, 0).unwrap_err();
1538 assert!(matches!(err, CommandError::InvalidNodeId(id) if id == bad));
1539 }
1540
1541 #[test]
1542 fn opstream_rejects_nested_root() {
1543 let empty = ArenaBuilder::<Mdast>::new(String::new()).finish();
1544
1545 let mut ok = Vec::new();
1547 op_open(&mut ok, MdastNodeType::Root);
1548 op_close(&mut ok);
1549 assert!(replay_mdast_opstream(&ok, &empty, 0).is_ok());
1550
1551 let mut ops = Vec::new();
1552 op_open(&mut ops, MdastNodeType::Root);
1553 op_open(&mut ops, MdastNodeType::Root);
1554 let err = replay_mdast_opstream(&ops, &empty, 0).unwrap_err();
1555 assert!(matches!(err, CommandError::UnencodableNodeType("root")));
1556 }
1557
1558 #[test]
1559 fn hast_opstream_rejects_doctype() {
1560 let empty = ArenaBuilder::<Hast>::new(String::new()).finish();
1561 let ops = vec![OP_OPEN, HastNodeType::Doctype as u8, OP_CLOSE];
1562 let err = replay_hast_opstream(&ops, &empty, 0).unwrap_err();
1563 assert!(matches!(err, CommandError::UnencodableNodeType("doctype")));
1564 }
1565
1566 #[test]
1567 fn opstream_rejects_over_deep_nesting() {
1568 let empty = ArenaBuilder::<Mdast>::new(String::new()).finish();
1571 let mut ops = Vec::new();
1572 for _ in 0..(MAX_OPSTREAM_DEPTH + 1) {
1573 op_open(&mut ops, MdastNodeType::Blockquote);
1574 }
1575 let err = replay_mdast_opstream(&ops, &empty, 0).unwrap_err();
1576 assert!(matches!(err, CommandError::OpstreamTooDeep(_)));
1577 }
1578
1579 #[test]
1580 fn set_property_rejects_out_of_range_or_unparseable_int() {
1581 let heading_id = 1;
1583
1584 let mut buf = Vec::new();
1585 push_set_property(&mut buf, heading_id, PROP_INT, "depth", "9999");
1586 let err =
1587 apply_mdast_commands(build_hello_world(), &buf, &test_parse_markdown).unwrap_err();
1588 assert!(matches!(err, CommandError::PropertyValueOutOfRange { .. }));
1589
1590 let mut buf = Vec::new();
1591 push_set_property(&mut buf, heading_id, PROP_INT, "depth", "not-a-number");
1592 let err =
1593 apply_mdast_commands(build_hello_world(), &buf, &test_parse_markdown).unwrap_err();
1594 assert!(matches!(err, CommandError::PropertyValueOutOfRange { .. }));
1595
1596 let mut buf = Vec::new();
1598 push_set_property(&mut buf, heading_id, PROP_INT, "depth", "255");
1599 let arena = apply_mdast_commands(build_hello_world(), &buf, &test_parse_markdown).unwrap();
1600 assert_eq!(
1601 decode_heading_data(arena.get_type_data(heading_id)).depth,
1602 255
1603 );
1604 }
1605
1606 #[cfg(feature = "mdx")]
1607 #[test]
1608 fn mdx_jsx_set_property_replaces_named_attrs_and_keeps_explicit() {
1609 use satteri_ast::shared::MDX_ATTR_EXPRESSION_PROP;
1610
1611 let mut b = ArenaBuilder::<Hast>::new(String::new());
1612 b.open_node(HastNodeType::Root as u8);
1613 b.open_node(HastNodeType::MdxJsxElement as u8);
1614 let elem_name = b.alloc_string("Box");
1615 let foo = b.alloc_string("foo");
1616 let expr = b.alloc_string("1+1");
1617 let rest = b.alloc_string("rest");
1618 let attrs = vec![
1619 (MDX_ATTR_EXPRESSION_PROP, foo, expr),
1620 (MDX_ATTR_SPREAD, StringRef::empty(), rest),
1621 ];
1622 b.set_data_current(&encode_mdx_jsx_element_data(elem_name, &attrs, true));
1623 b.close_node();
1624 b.close_node();
1625 let mut arena = b.finish();
1626
1627 apply_hast_mdx_jsx_attribute(&mut arena, 1, "foo", PROP_STRING, "x").unwrap();
1630 let data = arena.get_type_data(1).to_vec();
1631 assert_eq!(decode_mdx_jsx_attr_count(&data), 2);
1632 assert!(decode_mdx_jsx_explicit(&data));
1633 let (k0, _, _) = decode_mdx_jsx_attr(&data, 0);
1634 assert_eq!(k0, MDX_ATTR_SPREAD);
1635 let (k1, n1, v1) = decode_mdx_jsx_attr(&data, 1);
1636 assert_eq!(k1, MDX_ATTR_LITERAL_PROP);
1637 assert_eq!(arena.get_str(n1), "foo");
1638 assert_eq!(arena.get_str(v1), "x");
1639
1640 apply_hast_mdx_jsx_attribute(&mut arena, 1, "id", PROP_STRING, "intro").unwrap();
1642 let data = arena.get_type_data(1).to_vec();
1643 assert_eq!(decode_mdx_jsx_attr_count(&data), 3);
1644 assert!(decode_mdx_jsx_explicit(&data));
1645 }
1646
1647 fn test_parse_markdown(source: &str) -> Arena<Mdast> {
1648 let mut b = ArenaBuilder::<Mdast>::new(String::new());
1649 b.open_node(MdastNodeType::Root as u8);
1650 b.open_node(MdastNodeType::Paragraph as u8);
1651 b.open_node(MdastNodeType::Text as u8);
1652 let sref = b.alloc_string(source);
1653 b.set_data_current(&satteri_arena::encode_string_ref_data(sref));
1654 b.close_node();
1655 b.close_node();
1656 b.close_node();
1657 b.finish()
1658 }
1659
1660 fn push_u32(buf: &mut Vec<u8>, v: u32) {
1661 buf.extend_from_slice(&v.to_le_bytes());
1662 }
1663
1664 fn push_set_property(buf: &mut Vec<u8>, node_id: u32, value_type: u8, name: &str, value: &str) {
1666 buf.push(CMD_SET_PROPERTY);
1667 push_u32(buf, node_id);
1668 buf.push(value_type);
1669 push_u32(buf, name.len() as u32);
1670 buf.extend_from_slice(name.as_bytes());
1671 push_u32(buf, value.len() as u32);
1672 buf.extend_from_slice(value.as_bytes());
1673 }
1674
1675 fn build_hello_world() -> Arena<Mdast> {
1676 use satteri_ast::mdast::codec::{encode_heading_data, encode_string_ref_data};
1677
1678 let source = "# Hello\n\nWorld".to_string();
1679 let mut b = ArenaBuilder::<Mdast>::new(source);
1680
1681 b.open_node(MdastNodeType::Root as u8);
1682 b.set_position_current(0, 14, 1, 1, 2, 6);
1683
1684 b.open_node(MdastNodeType::Heading as u8);
1685 b.set_position_current(0, 7, 1, 1, 1, 8);
1686 b.set_data_current(&encode_heading_data(1));
1687
1688 b.open_node(MdastNodeType::Text as u8);
1689 b.set_position_current(2, 7, 1, 3, 1, 8);
1690 b.set_data_current(&encode_string_ref_data(StringRef::new(2, 5)));
1691 b.close_node();
1692
1693 b.close_node();
1694
1695 b.open_node(MdastNodeType::Paragraph as u8);
1696 b.set_position_current(9, 14, 2, 1, 2, 6);
1697
1698 b.open_node(MdastNodeType::Text as u8);
1699 b.set_position_current(9, 14, 2, 1, 2, 6);
1700 b.set_data_current(&encode_string_ref_data(StringRef::new(9, 5)));
1701 b.close_node();
1702
1703 b.close_node();
1704 b.close_node();
1705
1706 b.finish()
1707 }
1708
1709 #[test]
1710 fn empty_command_buffer() {
1711 let arena = build_hello_world();
1712 let result = apply_mdast_commands(arena.clone(), &[], &test_parse_markdown).unwrap();
1713 assert_eq!(result.len(), arena.len());
1714 }
1715
1716 #[test]
1717 fn remove_command() {
1718 let arena = build_hello_world();
1719 let heading_id = arena.get_children(0)[0];
1720 let mut buf = Vec::new();
1721 buf.push(CMD_REMOVE);
1722 push_u32(&mut buf, heading_id);
1723
1724 let result = apply_mdast_commands(arena.clone(), &buf, &test_parse_markdown).unwrap();
1725 assert_eq!(result.get_children(0).len(), 1);
1726 assert_eq!(
1727 result.get_node(result.get_children(0)[0]).node_type,
1728 MdastNodeType::Paragraph as u8
1729 );
1730 }
1731
1732 #[test]
1733 fn set_property_heading_depth() {
1734 let arena = build_hello_world();
1735 let heading_id = arena.get_children(0)[0];
1736
1737 let mut buf = Vec::new();
1738 push_set_property(&mut buf, heading_id, PROP_INT, "depth", "3");
1739
1740 let result = apply_mdast_commands(arena.clone(), &buf, &test_parse_markdown).unwrap();
1741 let heading_data = result.get_type_data(heading_id);
1742 let heading = decode_heading_data(heading_data);
1743 assert_eq!(heading.depth, 3);
1744 }
1745
1746 #[test]
1747 fn set_property_text_value() {
1748 let arena = build_hello_world();
1749 let heading_id = arena.get_children(0)[0];
1750 let text_id = arena.get_children(heading_id)[0];
1751
1752 let mut buf = Vec::new();
1753 push_set_property(&mut buf, text_id, PROP_STRING, "value", "Goodbye");
1754
1755 let result = apply_mdast_commands(arena.clone(), &buf, &test_parse_markdown).unwrap();
1756 let text_data = result.get_type_data(text_id);
1757 let sref = decode_string_ref_data(text_data);
1758 assert_eq!(result.get_str(sref), "Goodbye");
1759 }
1760
1761 #[test]
1762 fn replace_with_raw_markdown() {
1763 let arena = build_hello_world();
1764 let heading_id = arena.get_children(0)[0];
1765
1766 let raw_md = "## New Heading";
1767 let mut buf = Vec::new();
1768 buf.push(CMD_REPLACE);
1769 push_u32(&mut buf, heading_id);
1770 buf.push(PAYLOAD_RAW_MARKDOWN);
1771 push_u32(&mut buf, raw_md.len() as u32);
1772 buf.extend_from_slice(raw_md.as_bytes());
1773
1774 let result = apply_mdast_commands(arena.clone(), &buf, &test_parse_markdown).unwrap();
1775 let root_children = result.get_children(0);
1776 assert!(root_children.len() >= 2);
1777 }
1778
1779 #[test]
1780 fn multiple_commands() {
1781 let arena = build_hello_world();
1782 let heading_id = arena.get_children(0)[0];
1783 let text_id = arena.get_children(heading_id)[0];
1784
1785 let mut buf = Vec::new();
1786 push_set_property(&mut buf, heading_id, PROP_INT, "depth", "3");
1787 push_set_property(&mut buf, text_id, PROP_STRING, "value", "Hi");
1788
1789 let result = apply_mdast_commands(arena.clone(), &buf, &test_parse_markdown).unwrap();
1790
1791 let heading_data = result.get_type_data(heading_id);
1792 assert_eq!(decode_heading_data(heading_data).depth, 3);
1793
1794 let text_data = result.get_type_data(text_id);
1795 let sref = decode_string_ref_data(text_data);
1796 assert_eq!(result.get_str(sref), "Hi");
1797 }
1798
1799 #[test]
1800 fn set_property_null() {
1801 let arena = build_hello_world();
1802 let heading_id = arena.get_children(0)[0];
1803 let text_id = arena.get_children(heading_id)[0];
1804
1805 let mut buf = Vec::new();
1806 push_set_property(&mut buf, text_id, PROP_NULL, "value", "");
1807
1808 let result = apply_mdast_commands(arena.clone(), &buf, &test_parse_markdown).unwrap();
1809 let text_data = result.get_type_data(text_id);
1810 let sref = decode_string_ref_data(text_data);
1811 assert_eq!(sref.len, 0);
1812 }
1813
1814 #[test]
1815 fn set_property_invalid_field_reports_property_and_node_type() {
1816 let arena = build_hello_world();
1817 let heading_id = arena.get_children(0)[0];
1818
1819 let mut buf = Vec::new();
1820 push_set_property(&mut buf, heading_id, PROP_STRING, "value", "x");
1821
1822 let err = apply_mdast_commands(arena, &buf, &test_parse_markdown).unwrap_err();
1823 assert!(matches!(
1824 err,
1825 CommandError::UnknownField { ref name, ref node_type }
1826 if name == "value" && node_type == "heading"
1827 ));
1828 assert_eq!(
1829 err.to_string(),
1830 "cannot set property 'value' on a 'heading' node"
1831 );
1832 }
1833
1834 #[test]
1835 fn set_property_wrong_value_type_reports_value_mismatch() {
1836 let arena = build_hello_world();
1837 let heading_id = arena.get_children(0)[0];
1838
1839 let mut buf = Vec::new();
1841 push_set_property(&mut buf, heading_id, PROP_STRING, "depth", "3");
1842
1843 let err = apply_mdast_commands(arena, &buf, &test_parse_markdown).unwrap_err();
1844 assert!(matches!(
1845 err,
1846 CommandError::InvalidPropertyValue { ref name, ref node_type }
1847 if name == "depth" && node_type == "heading"
1848 ));
1849 assert_eq!(
1850 err.to_string(),
1851 "property 'depth' on a 'heading' node cannot hold a value of this type"
1852 );
1853 }
1854
1855 fn build_single_node(node_type: MdastNodeType, type_data: &[u8]) -> Arena<Mdast> {
1857 let mut b = ArenaBuilder::<Mdast>::new(String::new());
1858 b.open_node(MdastNodeType::Root as u8);
1859 b.open_node(node_type as u8);
1860 b.set_data_current(type_data);
1861 b.close_node();
1862 b.close_node();
1863 b.finish()
1864 }
1865
1866 #[test]
1867 fn set_property_image_reference_alt_roundtrip() {
1868 let mut b = ArenaBuilder::<Mdast>::new(String::new());
1869 b.open_node(MdastNodeType::Root as u8);
1870 b.open_node(MdastNodeType::ImageReference as u8);
1871 let identifier = b.alloc_string("img");
1872 let alt = b.alloc_string("old");
1873 b.set_data_current(&encode_image_reference_data(identifier, identifier, 0, alt));
1874 b.close_node();
1875 b.close_node();
1876 let arena = b.finish();
1877 let image_ref_id = arena.get_children(0)[0];
1878
1879 let mut buf = Vec::new();
1880 push_set_property(&mut buf, image_ref_id, PROP_STRING, "alt", "new alt");
1881
1882 let result = apply_mdast_commands(arena, &buf, &test_parse_markdown).unwrap();
1883 let alt = decode_image_reference_alt(result.get_type_data(image_ref_id));
1884 assert_eq!(result.get_str(alt), "new alt");
1885 }
1886
1887 #[test]
1888 fn set_property_reference_type_valid_and_invalid() {
1889 let mut b = ArenaBuilder::<Mdast>::new(String::new());
1890 b.open_node(MdastNodeType::Root as u8);
1891 b.open_node(MdastNodeType::LinkReference as u8);
1892 let identifier = b.alloc_string("ref");
1893 b.set_data_current(&encode_reference_data(identifier, identifier, 0));
1894 b.close_node();
1895 b.close_node();
1896 let arena = b.finish();
1897 let link_ref_id = arena.get_children(0)[0];
1898
1899 let mut buf = Vec::new();
1900 push_set_property(&mut buf, link_ref_id, PROP_STRING, "referenceType", "full");
1901 let result = apply_mdast_commands(arena.clone(), &buf, &test_parse_markdown).unwrap();
1902 let reference = decode_reference_data(result.get_type_data(link_ref_id));
1903 assert_eq!(reference.reference_kind, 2);
1904
1905 let mut buf = Vec::new();
1907 push_set_property(&mut buf, link_ref_id, PROP_STRING, "referenceType", "bogus");
1908 let err = apply_mdast_commands(arena, &buf, &test_parse_markdown).unwrap_err();
1909 assert!(matches!(
1910 err,
1911 CommandError::InvalidPropertyValue { ref name, ref node_type }
1912 if name == "referenceType" && node_type == "linkReference"
1913 ));
1914 }
1915
1916 #[test]
1917 fn set_property_list_start_and_ordered() {
1918 let arena = build_single_node(MdastNodeType::List, &encode_list_data(false, 1, false));
1919 let list_id = arena.get_children(0)[0];
1920
1921 let mut buf = Vec::new();
1922 push_set_property(&mut buf, list_id, PROP_INT, "start", "5");
1923 push_set_property(&mut buf, list_id, PROP_BOOL_TRUE, "ordered", "");
1924
1925 let result = apply_mdast_commands(arena, &buf, &test_parse_markdown).unwrap();
1926 let list = decode_list_data(result.get_type_data(list_id));
1927 assert_eq!(list.start, 5);
1928 assert!(list.ordered);
1929 assert!(!list.spread);
1930 }
1931
1932 #[test]
1933 fn escape_braces_in_html_text_basic() {
1934 assert_eq!(
1935 escape_braces_in_html_text("<span>{foo: 1}</span>"),
1936 "<span>{'{'}foo: 1{'}'}</span>"
1937 );
1938 }
1939
1940 #[test]
1941 fn escape_braces_preserves_attributes() {
1942 let result = escape_braces_in_html_text(r#"<span data-x="{a}">{b}</span>"#);
1943 assert!(
1944 result.contains(r#"data-x="{a}""#),
1945 "attribute braces preserved"
1946 );
1947 assert!(result.contains("{'{'}"), "text braces escaped");
1948 }
1949
1950 #[test]
1951 fn escape_braces_no_braces() {
1952 let html = r#"<pre class="shiki"><code><span style="color:red">hello</span></code></pre>"#;
1953 assert_eq!(escape_braces_in_html_text(html), html);
1954 }
1955
1956 #[test]
1957 fn escape_braces_shiki_output() {
1958 let html = r#"<pre class="shiki"><code><span style="color:#E1E4E8">const x = </span><span style="color:#B392F0">{</span><span style="color:#E1E4E8">foo: 1</span><span style="color:#B392F0">}</span></code></pre>"#;
1959 let escaped = escape_braces_in_html_text(html);
1960 assert!(
1961 !escaped.contains(">{<"),
1962 "bare braces in text should be escaped"
1963 );
1964 assert!(
1965 !escaped.contains(">}<"),
1966 "bare braces in text should be escaped"
1967 );
1968 assert!(escaped.contains(r#"class="shiki""#));
1969 assert!(escaped.contains(r#"style="color:#E1E4E8""#));
1970 }
1971
1972 #[test]
1973 fn hast_set_property_add_new() {
1974 let arena = build_hast_element(&[]);
1975 let element_id = arena.get_children(0)[0];
1976
1977 let mut buf = Vec::new();
1978 push_set_property(&mut buf, element_id, PROP_STRING, "class", "test");
1979
1980 let result = apply_hast_commands(arena.clone(), &buf).unwrap();
1981 let data = result.get_type_data(element_id);
1982 let prop_count = u32::from_le_bytes(data[8..12].try_into().unwrap());
1983 assert_eq!(prop_count, 1);
1984 let name_ref = StringRef::new(
1985 u32::from_le_bytes(data[16..20].try_into().unwrap()),
1986 u32::from_le_bytes(data[20..24].try_into().unwrap()),
1987 );
1988 assert_eq!(result.get_str(name_ref), "class");
1989 let val_ref = StringRef::new(
1990 u32::from_le_bytes(data[28..32].try_into().unwrap()),
1991 u32::from_le_bytes(data[32..36].try_into().unwrap()),
1992 );
1993 assert_eq!(result.get_str(val_ref), "test");
1994 assert_eq!(data[24], PROP_STRING);
1995 }
1996
1997 #[test]
1998 fn hast_set_property_overwrite_existing() {
1999 let arena = build_hast_element(&[("class", PROP_STRING, "old")]);
2000 let element_id = arena.get_children(0)[0];
2001
2002 let mut buf = Vec::new();
2003 push_set_property(&mut buf, element_id, PROP_STRING, "class", "new-value");
2004
2005 let result = apply_hast_commands(arena.clone(), &buf).unwrap();
2006 let data = result.get_type_data(element_id);
2007 let prop_count = u32::from_le_bytes(data[8..12].try_into().unwrap());
2008 assert_eq!(prop_count, 1);
2009 let val_ref = StringRef::new(
2010 u32::from_le_bytes(data[28..32].try_into().unwrap()),
2011 u32::from_le_bytes(data[32..36].try_into().unwrap()),
2012 );
2013 assert_eq!(result.get_str(val_ref), "new-value");
2014 }
2015
2016 #[test]
2017 fn hast_set_property_bool_true() {
2018 let arena = build_hast_element(&[]);
2019 let element_id = arena.get_children(0)[0];
2020
2021 let mut buf = Vec::new();
2022 push_set_property(&mut buf, element_id, PROP_BOOL_TRUE, "disabled", "");
2023
2024 let result = apply_hast_commands(arena.clone(), &buf).unwrap();
2025 let data = result.get_type_data(element_id);
2026 let prop_count = u32::from_le_bytes(data[8..12].try_into().unwrap());
2027 assert_eq!(prop_count, 1);
2028 assert_eq!(data[24], PROP_BOOL_TRUE);
2029 }
2030
2031 #[test]
2032 fn hast_set_property_multiple_on_same_node() {
2033 let arena = build_hast_element(&[]);
2034 let element_id = arena.get_children(0)[0];
2035
2036 let mut buf = Vec::new();
2037 push_set_property(&mut buf, element_id, PROP_STRING, "class", "foo");
2038 push_set_property(&mut buf, element_id, PROP_STRING, "id", "bar");
2039
2040 let result = apply_hast_commands(arena.clone(), &buf).unwrap();
2041 let data = result.get_type_data(element_id);
2042 let prop_count = u32::from_le_bytes(data[8..12].try_into().unwrap());
2043 assert_eq!(prop_count, 2);
2044 }
2045
2046 fn build_hast_element(props: &[(&str, u8, &str)]) -> Arena<Hast> {
2048 use satteri_ast::hast::node::HastNodeType;
2049
2050 let mut b = ArenaBuilder::<Hast>::new(String::new());
2051 b.open_node_raw(HastNodeType::Root as u8);
2052 b.open_node_raw(HastNodeType::Element as u8);
2053 let tag_ref = b.alloc_string("div");
2054 let prop_tuples: Vec<(StringRef, u8, StringRef)> = props
2055 .iter()
2056 .map(|(name, kind, value)| {
2057 let n = b.alloc_string(name);
2058 let v = if value.is_empty() {
2059 StringRef::empty()
2060 } else {
2061 b.alloc_string(value)
2062 };
2063 (n, *kind, v)
2064 })
2065 .collect();
2066 let mut type_data = Vec::with_capacity(16 + prop_tuples.len() * 20);
2067 type_data.extend_from_slice(&tag_ref.offset.to_le_bytes());
2068 type_data.extend_from_slice(&tag_ref.len.to_le_bytes());
2069 type_data.extend_from_slice(&(prop_tuples.len() as u32).to_le_bytes());
2070 type_data.extend_from_slice(&0u32.to_le_bytes());
2071 for (n, kind, v) in &prop_tuples {
2072 type_data.extend_from_slice(&n.offset.to_le_bytes());
2073 type_data.extend_from_slice(&n.len.to_le_bytes());
2074 type_data.push(*kind);
2075 type_data.extend_from_slice(&[0u8; 3]);
2076 type_data.extend_from_slice(&v.offset.to_le_bytes());
2077 type_data.extend_from_slice(&v.len.to_le_bytes());
2078 }
2079 b.set_data_current(&type_data);
2080 b.close_node();
2081 b.close_node();
2082 b.finish()
2083 }
2084}