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
300fn emit_ref_node<K: ArenaKind>(ref_id: u32, builder: &mut ArenaBuilder<K>) {
304 builder.open_node_raw(REF_NODE_TYPE);
305 builder.set_data_current(&ref_id.to_le_bytes());
306 builder.close_node();
307}
308
309use crate::generated::encode::{
312 encode_hast_tail_from_ops, encode_mdast_tail_from_ops, encode_mdast_type_data_from_ops,
313 MAX_FIXED_TYPE_DATA,
314};
315
316pub(crate) fn alloc_opt_str<K: ArenaKind>(
317 builder: &mut ArenaBuilder<K>,
318 s: Option<&str>,
319) -> StringRef {
320 match s {
321 Some(v) if !v.is_empty() => builder.alloc_string(v),
322 _ => StringRef::empty(),
323 }
324}
325
326fn apply_hast_set_property(
332 arena: &mut Arena<Hast>,
333 node_id: u32,
334 prop_name: &str,
335 value_type: u8,
336 value_str: &str,
337) -> Result<(), CommandError> {
338 if node_id as usize >= arena.len() {
339 return Err(CommandError::InvalidNodeId(node_id));
340 }
341 if prop_name == "data" {
342 apply_data_property(arena, node_id, value_type, value_str);
343 return Ok(());
344 }
345
346 let raw_type = arena.get_node(node_id).node_type;
347 let node_type = HastNodeType::from_u8(raw_type)
348 .ok_or_else(|| CommandError::UnknownNodeType(format!("hast type 0x{raw_type:02x}")))?;
349
350 match node_type {
351 HastNodeType::Element => {
352 apply_hast_element_property(arena, node_id, prop_name, value_type, value_str)
353 }
354
355 HastNodeType::Text
356 | HastNodeType::Comment
357 | HastNodeType::Raw
358 | HastNodeType::MdxFlowExpression
359 | HastNodeType::MdxTextExpression
360 | HastNodeType::MdxEsm
361 if prop_name == "value" =>
362 {
363 let sref = arena.alloc_string(value_str);
364 let data = arena.get_type_data(node_id);
365 if data.len() >= 8 {
366 let data_offset = arena.get_node(node_id).data_offset as usize;
367 arena.type_data[data_offset..data_offset + 8].copy_from_slice(&sref.as_bytes());
368 Ok(())
369 } else {
370 Err(CommandError::TypeDataTooShort)
371 }
372 }
373
374 #[cfg(feature = "mdx")]
375 HastNodeType::MdxJsxElement | HastNodeType::MdxJsxTextElement => {
376 apply_hast_mdx_jsx_attribute(arena, node_id, prop_name, value_type, value_str)
377 }
378
379 _ => Err(CommandError::UnknownField {
380 node_type: node_type.name().to_string(),
381 name: prop_name.to_string(),
382 }),
383 }
384}
385
386#[cfg(feature = "mdx")]
400fn apply_hast_mdx_jsx_attribute(
401 arena: &mut Arena<Hast>,
402 node_id: u32,
403 attr_name: &str,
404 value_type: u8,
405 value_str: &str,
406) -> Result<(), CommandError> {
407 let old_data = arena.get_type_data(node_id).to_vec();
408 if old_data.len() < 16 {
409 return Err(CommandError::TypeDataTooShort);
410 }
411 let elem_name = decode_mdx_jsx_element_name(&old_data);
412 let explicit = decode_mdx_jsx_explicit(&old_data);
413 let attr_count = decode_mdx_jsx_attr_count(&old_data);
414
415 let (kind, val_ref) = match value_type {
417 PROP_BOOL_TRUE | PROP_NULL => (MDX_ATTR_BOOLEAN_PROP, StringRef::empty()),
418 PROP_BOOL_FALSE => (MDX_ATTR_LITERAL_PROP, arena.alloc_string("false")),
419 _ if value_str.is_empty() => (MDX_ATTR_LITERAL_PROP, StringRef::empty()),
420 _ => (MDX_ATTR_LITERAL_PROP, arena.alloc_string(value_str)),
421 };
422
423 let mut attrs: Vec<(u8, StringRef, StringRef)> = Vec::with_capacity(attr_count as usize + 1);
424 let mut name_ref: Option<StringRef> = None;
425 for i in 0..attr_count {
426 let (existing_kind, existing_name, existing_value) = decode_mdx_jsx_attr(&old_data, i);
427 if existing_kind != MDX_ATTR_SPREAD && arena.get_str(existing_name) == attr_name {
428 name_ref = Some(existing_name);
429 continue;
430 }
431 attrs.push((existing_kind, existing_name, existing_value));
432 }
433 let name_ref = name_ref.unwrap_or_else(|| arena.alloc_string(attr_name));
434 attrs.push((kind, name_ref, val_ref));
435
436 arena.set_type_data(
437 node_id,
438 &encode_mdx_jsx_element_data(elem_name, &attrs, explicit),
439 );
440 Ok(())
441}
442
443fn apply_hast_element_property(
445 arena: &mut Arena<Hast>,
446 node_id: u32,
447 prop_name: &str,
448 value_type: u8,
449 value_str: &str,
450) -> Result<(), CommandError> {
451 let old_data = arena.get_type_data(node_id).to_vec();
452 if old_data.len() < 16 {
453 return Err(CommandError::TypeDataTooShort);
454 }
455
456 let old_prop_count = u32::from_le_bytes(old_data[8..12].try_into().unwrap()) as usize;
457
458 let mut found_index: Option<usize> = None;
459 for i in 0..old_prop_count {
460 let base = 16 + i * 20;
461 let name_off = u32::from_le_bytes(old_data[base..base + 4].try_into().unwrap());
462 let name_len = u32::from_le_bytes(old_data[base + 4..base + 8].try_into().unwrap());
463 let existing_name = arena.get_str(StringRef::new(name_off, name_len));
464 if existing_name == prop_name {
465 found_index = Some(i);
466 break;
467 }
468 }
469
470 let name_ref = arena.alloc_string(prop_name);
471 let val_ref = if value_str.is_empty() {
472 StringRef::empty()
473 } else {
474 arena.alloc_string(value_str)
475 };
476
477 if let Some(idx) = found_index {
478 let mut new_data = old_data;
479 let base = 16 + idx * 20;
480 new_data[base..base + 4].copy_from_slice(&name_ref.offset.to_le_bytes());
481 new_data[base + 4..base + 8].copy_from_slice(&name_ref.len.to_le_bytes());
482 new_data[base + 8] = value_type;
483 new_data[base + 9..base + 12].copy_from_slice(&[0u8; 3]);
484 new_data[base + 12..base + 16].copy_from_slice(&val_ref.offset.to_le_bytes());
485 new_data[base + 16..base + 20].copy_from_slice(&val_ref.len.to_le_bytes());
486 arena.set_type_data(node_id, &new_data);
487 } else {
488 let new_prop_count = (old_prop_count + 1) as u32;
489 let mut new_data = Vec::with_capacity(16 + new_prop_count as usize * 20);
490 new_data.extend_from_slice(&old_data[0..8]);
491 new_data.extend_from_slice(&new_prop_count.to_le_bytes());
492 new_data.extend_from_slice(&0u32.to_le_bytes());
493 if old_prop_count > 0 {
494 new_data.extend_from_slice(&old_data[16..16 + old_prop_count * 20]);
495 }
496 new_data.extend_from_slice(&name_ref.offset.to_le_bytes());
497 new_data.extend_from_slice(&name_ref.len.to_le_bytes());
498 new_data.push(value_type);
499 new_data.extend_from_slice(&[0u8; 3]);
500 new_data.extend_from_slice(&val_ref.offset.to_le_bytes());
501 new_data.extend_from_slice(&val_ref.len.to_le_bytes());
502 arena.set_type_data(node_id, &new_data);
503 }
504
505 Ok(())
506}
507
508trait OpCollector<'a>: Sized {
519 type Kind: ArenaKind;
520 const NUMERIC_OPS: bool;
524
525 fn open(node_type: u8) -> Self;
526 fn check_tag(tag: u8) -> Result<(), CommandError>;
527 fn finalize(&mut self, builder: &mut ArenaBuilder<Self::Kind>);
528 fn str_field(&mut self, field: u8, value: &'a str);
529 fn bool_field(&mut self, field: u8, value: bool);
530 fn prop(&mut self, name: &'a str, kind: u8, value: &'a str);
531 fn data(&mut self, bytes: &'a [u8]);
532 fn u8_field(&mut self, _field: u8, _value: u8) {}
533 fn u32_field(&mut self, _field: u8, _value: u32) {}
534 fn align(&mut self, _bytes: &'a [u8]) {}
535}
536
537const MAX_OPSTREAM_DEPTH: usize = 128;
541
542fn replay_opstream<'a, C: OpCollector<'a>>(
545 ops: &'a [u8],
546 orig: &Arena<C::Kind>,
547 anchor: u32,
548) -> Result<Arena<C::Kind>, CommandError> {
549 let mut builder = ArenaBuilder::<C::Kind>::new(String::new());
550 let mut reader = BufReader::new(ops);
551 let mut stack: Vec<C> = Vec::new();
552
553 while reader.remaining() > 0 {
554 match reader.read_u8()? {
555 OP_OPEN => {
556 if let Some(c) = stack.last_mut() {
557 c.finalize(&mut builder);
558 }
559 let node_type = reader.read_u8()?;
560 C::check_tag(node_type)?;
561 if !stack.is_empty() && node_type == <C::Kind as ArenaKind>::ROOT_TAG {
565 return Err(CommandError::UnencodableNodeType("root"));
566 }
567 if stack.len() >= MAX_OPSTREAM_DEPTH {
568 return Err(CommandError::OpstreamTooDeep(MAX_OPSTREAM_DEPTH));
569 }
570 builder.open_node(node_type);
571 stack.push(C::open(node_type));
572 }
573 OP_CLOSE => {
574 let Some(mut c) = stack.pop() else {
575 return Err(CommandError::UnbalancedOpstream);
576 };
577 c.finalize(&mut builder);
578 builder.close_node();
579 }
580 OP_REF => {
581 if let Some(c) = stack.last_mut() {
582 c.finalize(&mut builder);
583 }
584 let id = reader.read_u32()?;
585 if id as usize >= orig.len() {
588 return Err(CommandError::InvalidNodeId(id));
589 }
590 emit_ref_node(id, &mut builder);
591 }
592 OP_KEEP_CHILDREN => {
593 if let Some(c) = stack.last_mut() {
594 c.finalize(&mut builder);
595 }
596 if anchor as usize >= orig.len() {
597 return Err(CommandError::InvalidNodeId(anchor));
598 }
599 for &child in orig.get_children(anchor) {
600 emit_ref_node(child, &mut builder);
601 }
602 }
603 OP_STR => {
604 let field = reader.read_u8()?;
605 let len = reader.read_u32()? as usize;
606 let value = reader.read_str(len)?;
607 if let Some(c) = stack.last_mut() {
608 c.str_field(field, value);
609 }
610 }
611 OP_U8 if C::NUMERIC_OPS => {
612 let field = reader.read_u8()?;
613 let value = reader.read_u8()?;
614 if let Some(c) = stack.last_mut() {
615 c.u8_field(field, value);
616 }
617 }
618 OP_U32 if C::NUMERIC_OPS => {
619 let field = reader.read_u8()?;
620 let value = reader.read_u32()?;
621 if let Some(c) = stack.last_mut() {
622 c.u32_field(field, value);
623 }
624 }
625 OP_BOOL => {
626 let field = reader.read_u8()?;
627 let value = reader.read_u8()? != 0;
628 if let Some(c) = stack.last_mut() {
629 c.bool_field(field, value);
630 }
631 }
632 OP_PROP => {
633 let name_len = reader.read_u32()? as usize;
634 let name = reader.read_str(name_len)?;
635 let kind = reader.read_u8()?;
636 let val_len = reader.read_u32()? as usize;
637 let value = reader.read_str(val_len)?;
638 if let Some(c) = stack.last_mut() {
639 c.prop(name, kind, value);
640 }
641 }
642 OP_ALIGN if C::NUMERIC_OPS => {
643 let len = reader.read_u32()? as usize;
644 let bytes = reader.read_bytes(len)?;
645 if let Some(c) = stack.last_mut() {
646 c.align(bytes);
647 }
648 }
649 OP_DATA => {
650 let len = reader.read_u32()? as usize;
651 let bytes = reader.read_bytes(len)?;
652 if let Some(c) = stack.last_mut() {
653 c.data(bytes);
654 }
655 }
656 other => return Err(CommandError::UnknownCommand(other)),
657 }
658 }
659
660 if !stack.is_empty() {
661 return Err(CommandError::UnbalancedOpstream);
662 }
663 Ok(builder.finish())
664}
665
666#[cfg(feature = "mdx")]
669pub(crate) fn intern_mdx_jsx_attrs<K: ArenaKind>(
670 props: &[(&str, u8, &str)],
671 builder: &mut ArenaBuilder<K>,
672) -> Vec<(u8, StringRef, StringRef)> {
673 let mut attrs = Vec::with_capacity(props.len());
674 for &(name, kind, value) in props {
675 let nr = if kind == MDX_ATTR_SPREAD {
676 StringRef::empty()
677 } else {
678 builder.alloc_string(name)
679 };
680 let vr = if kind == MDX_ATTR_BOOLEAN_PROP {
681 StringRef::empty()
682 } else {
683 builder.alloc_string(value)
684 };
685 attrs.push((kind, nr, vr));
686 }
687 attrs
688}
689
690#[derive(Default)]
693pub(crate) struct FieldCollector<'a> {
694 node_type: u8,
695 finalized: bool,
696 pub(crate) strs: [Option<&'a str>; OF_FIELD_COUNT],
697 pub(crate) depth: Option<u8>,
698 checked: Option<u8>,
699 start: Option<u32>,
700 ordered: Option<bool>,
701 spread: Option<bool>,
702 pub(crate) props: Vec<(&'a str, u8, &'a str)>,
704 pub(crate) align: Option<&'a [u8]>,
706 pub(crate) explicit: Option<bool>,
708 data: Option<&'a [u8]>,
709}
710
711fn finalize_collector(c: &mut FieldCollector<'_>, builder: &mut ArenaBuilder<Mdast>) {
713 const LIST: u8 = MdastNodeType::List as u8;
714 const LIST_ITEM: u8 = MdastNodeType::ListItem as u8;
715
716 if c.finalized {
717 return;
718 }
719 c.finalized = true;
720 let mut fixed = [0u8; MAX_FIXED_TYPE_DATA];
721 if let Some(len) = encode_mdast_type_data_from_ops(c, c.node_type, builder, &mut fixed) {
722 builder.set_data_current(&fixed[..len]);
723 } else if let Some(type_data) = encode_mdast_tail_from_ops(c, c.node_type, builder) {
724 builder.set_data_current(&type_data);
726 } else {
727 let type_data: Vec<u8> = match c.node_type {
728 LIST => encode_list_data(
729 c.ordered.unwrap_or(false),
730 c.start.unwrap_or(1),
731 c.spread.unwrap_or(false),
732 ),
733 LIST_ITEM => encode_list_item_data(c.checked.unwrap_or(2), c.spread.unwrap_or(false)),
735 _ => Vec::new(),
737 };
738 if !type_data.is_empty() {
739 builder.set_data_current(&type_data);
740 }
741 }
742 if let Some(data) = c.data {
743 let id = builder.current_node_id();
744 builder.arena_mut().set_node_data(id, data.to_vec());
745 }
746}
747
748impl<'a> OpCollector<'a> for FieldCollector<'a> {
749 type Kind = Mdast;
750 const NUMERIC_OPS: bool = true;
751
752 fn open(node_type: u8) -> Self {
753 FieldCollector {
754 node_type,
755 ..Default::default()
756 }
757 }
758
759 fn check_tag(tag: u8) -> Result<(), CommandError> {
762 let known = MdastNodeType::from_u8(tag).is_some();
763 #[cfg(not(feature = "mdx"))]
764 let known = known
765 && !matches!(
766 MdastNodeType::from_u8(tag),
767 Some(
768 MdastNodeType::MdxJsxFlowElement
769 | MdastNodeType::MdxJsxTextElement
770 | MdastNodeType::MdxFlowExpression
771 | MdastNodeType::MdxTextExpression
772 | MdastNodeType::MdxjsEsm
773 )
774 );
775 if known {
776 Ok(())
777 } else {
778 Err(CommandError::UnknownNodeType(format!(
779 "op-stream tag {tag}"
780 )))
781 }
782 }
783
784 fn finalize(&mut self, builder: &mut ArenaBuilder<Mdast>) {
785 finalize_collector(self, builder);
786 }
787
788 fn str_field(&mut self, field: u8, value: &'a str) {
789 let field = field as usize;
790 if field < self.strs.len() {
791 self.strs[field] = Some(value);
792 }
793 }
794
795 fn bool_field(&mut self, field: u8, value: bool) {
796 match field {
797 OF_ORDERED => self.ordered = Some(value),
798 OF_SPREAD => self.spread = Some(value),
799 OF_EXPLICIT => self.explicit = Some(value),
800 _ => {}
801 }
802 }
803
804 fn prop(&mut self, name: &'a str, kind: u8, value: &'a str) {
805 self.props.push((name, kind, value));
806 }
807
808 fn data(&mut self, bytes: &'a [u8]) {
809 self.data = Some(bytes);
810 }
811
812 fn u8_field(&mut self, field: u8, value: u8) {
813 match field {
814 OF_DEPTH => self.depth = Some(value),
815 OF_CHECKED => self.checked = Some(value),
816 _ => {}
817 }
818 }
819
820 fn u32_field(&mut self, field: u8, value: u32) {
821 if field == OF_START {
822 self.start = Some(value);
823 }
824 }
825
826 fn align(&mut self, bytes: &'a [u8]) {
827 self.align = Some(bytes);
828 }
829}
830
831fn replay_mdast_opstream(
832 ops: &[u8],
833 orig: &Arena<Mdast>,
834 anchor: u32,
835) -> Result<Arena<Mdast>, CommandError> {
836 replay_opstream::<FieldCollector>(ops, orig, anchor)
837}
838
839fn read_mdast_payload(
843 reader: &mut BufReader<'_>,
844 parse_markdown: &dyn Fn(&str) -> Arena<Mdast>,
845 orig: &Arena<Mdast>,
846 anchor: u32,
847) -> Result<(Arena<Mdast>, bool), CommandError> {
848 let payload_type = reader.read_u8()?;
849 let len = reader.read_u32()? as usize;
850
851 match payload_type {
852 PAYLOAD_RAW_MARKDOWN => {
853 let md = reader.read_str(len)?;
854 Ok((parse_markdown(md), false))
855 }
856 PAYLOAD_RAW_HTML => {
857 let html = reader.read_str(len)?;
858 let escaped = escape_braces_in_html_text(html);
859 Ok((parse_markdown(&escaped), false))
860 }
861 PAYLOAD_OPSTREAM => {
862 let ops = reader.read_bytes(len)?;
863 Ok((replay_mdast_opstream(ops, orig, anchor)?, false))
864 }
865 other => Err(CommandError::UnknownPayloadType(other)),
866 }
867}
868
869#[derive(Default)]
873pub(crate) struct HastFieldCollector<'a> {
874 node_type: u8,
875 finalized: bool,
876 pub(crate) tag: Option<&'a str>,
878 value: Option<&'a str>,
879 pub(crate) props: Vec<(&'a str, u8, &'a str)>,
880 pub(crate) explicit: Option<bool>,
882 data: Option<&'a [u8]>,
883}
884
885fn finalize_hast_collector(c: &mut HastFieldCollector<'_>, builder: &mut ArenaBuilder<Hast>) {
886 const TEXT: u8 = HastNodeType::Text as u8;
887 const COMMENT: u8 = HastNodeType::Comment as u8;
888 const RAW: u8 = HastNodeType::Raw as u8;
889 const MDX_FLOW_EXPRESSION: u8 = HastNodeType::MdxFlowExpression as u8;
890 const MDX_ESM: u8 = HastNodeType::MdxEsm as u8;
891 const MDX_TEXT_EXPRESSION: u8 = HastNodeType::MdxTextExpression as u8;
892
893 if c.finalized {
894 return;
895 }
896 c.finalized = true;
897 if let Some(type_data) = encode_hast_tail_from_ops(c, c.node_type, builder) {
898 builder.set_data_current(&type_data);
900 } else {
901 let type_data: Vec<u8> = match c.node_type {
902 TEXT | COMMENT | RAW | MDX_FLOW_EXPRESSION | MDX_ESM | MDX_TEXT_EXPRESSION => {
903 let sref = builder.alloc_string(c.value.unwrap_or(""));
904 encode_string_ref_data(sref)
905 }
906 _ => Vec::new(),
908 };
909 if !type_data.is_empty() {
910 builder.set_data_current(&type_data);
911 }
912 }
913 if let Some(data) = c.data {
914 let id = builder.current_node_id();
915 builder.arena_mut().set_node_data(id, data.to_vec());
916 }
917}
918
919impl<'a> OpCollector<'a> for HastFieldCollector<'a> {
920 type Kind = Hast;
921 const NUMERIC_OPS: bool = false;
922
923 fn open(node_type: u8) -> Self {
924 HastFieldCollector {
925 node_type,
926 ..Default::default()
927 }
928 }
929
930 fn check_tag(tag: u8) -> Result<(), CommandError> {
932 if HastNodeType::from_u8(tag) == Some(HastNodeType::Doctype) {
936 return Err(CommandError::UnencodableNodeType("doctype"));
937 }
938 let known = HastNodeType::from_u8(tag).is_some();
939 #[cfg(not(feature = "mdx"))]
940 let known = known
941 && !matches!(
942 HastNodeType::from_u8(tag),
943 Some(
944 HastNodeType::MdxJsxElement
945 | HastNodeType::MdxJsxTextElement
946 | HastNodeType::MdxFlowExpression
947 | HastNodeType::MdxEsm
948 | HastNodeType::MdxTextExpression
949 )
950 );
951 if known {
952 Ok(())
953 } else {
954 Err(CommandError::UnknownNodeType(format!(
955 "op-stream tag {tag}"
956 )))
957 }
958 }
959
960 fn finalize(&mut self, builder: &mut ArenaBuilder<Hast>) {
961 finalize_hast_collector(self, builder);
962 }
963
964 fn str_field(&mut self, field: u8, value: &'a str) {
965 match field {
966 OF_TAGNAME | OF_NAME => self.tag = Some(value),
967 OF_VALUE => self.value = Some(value),
968 _ => {}
969 }
970 }
971
972 fn bool_field(&mut self, field: u8, value: bool) {
973 if field == OF_EXPLICIT {
974 self.explicit = Some(value);
975 }
976 }
977
978 fn prop(&mut self, name: &'a str, kind: u8, value: &'a str) {
979 self.props.push((name, kind, value));
980 }
981
982 fn data(&mut self, bytes: &'a [u8]) {
983 self.data = Some(bytes);
984 }
985}
986
987fn replay_hast_opstream(
988 ops: &[u8],
989 orig: &Arena<Hast>,
990 anchor: u32,
991) -> Result<Arena<Hast>, CommandError> {
992 replay_opstream::<HastFieldCollector>(ops, orig, anchor)
993}
994
995fn read_hast_payload(
999 reader: &mut BufReader<'_>,
1000 orig: &Arena<Hast>,
1001 anchor: u32,
1002) -> Result<(Arena<Hast>, bool), CommandError> {
1003 let payload_type = reader.read_u8()?;
1004 let len = reader.read_u32()? as usize;
1005
1006 match payload_type {
1007 PAYLOAD_OPSTREAM => {
1008 let ops = reader.read_bytes(len)?;
1009 Ok((replay_hast_opstream(ops, orig, anchor)?, false))
1010 }
1011 other => Err(CommandError::UnknownPayloadType(other)),
1012 }
1013}
1014
1015pub fn apply_mdast_commands(
1038 arena: Arena<Mdast>,
1039 command_buf: &[u8],
1040 parse_markdown: &dyn Fn(&str) -> Arena<Mdast>,
1041) -> Result<Arena<Mdast>, CommandError> {
1042 let (arena, dropped) = apply_mdast_commands_lenient(arena, command_buf, parse_markdown)?;
1043 if let Some(anchor) = dropped.first() {
1044 return Err(CommandError::PatchOnRemovedSubtree(*anchor));
1045 }
1046 Ok(arena)
1047}
1048
1049pub fn apply_mdast_commands_lenient(
1056 mut arena: Arena<Mdast>,
1057 command_buf: &[u8],
1058 parse_markdown: &dyn Fn(&str) -> Arena<Mdast>,
1059) -> Result<(Arena<Mdast>, Vec<u32>), CommandError> {
1060 if command_buf.is_empty() {
1061 return Ok((arena, Vec::new()));
1062 }
1063
1064 let mut patches: Vec<Patch<Mdast>> = Vec::new();
1065 let mut reader = BufReader::new(command_buf);
1066
1067 while reader.remaining() > 0 {
1068 let cmd = reader.read_u8()?;
1069
1070 match cmd {
1071 CMD_REMOVE => {
1072 let node_id = reader.read_u32()?;
1073 patches.push(Patch::Remove { node_id });
1074 }
1075
1076 CMD_SET_PROPERTY => {
1077 let node_id = reader.read_u32()?;
1078 let value_type = reader.read_u8()?;
1079 let name_len = reader.read_u32()? as usize;
1080 let name = reader.read_str(name_len)?;
1081 let value_len = reader.read_u32()? as usize;
1082 let value = reader.read_str(value_len)?;
1083 apply_mdast_set_property(&mut arena, node_id, name, value_type, value)?;
1084 }
1085
1086 CMD_INSERT_BEFORE => {
1087 let node_id = reader.read_u32()?;
1088 let (new_tree, _) =
1089 read_mdast_payload(&mut reader, parse_markdown, &arena, node_id)?;
1090 patches.push(Patch::InsertBefore { node_id, new_tree });
1091 }
1092
1093 CMD_INSERT_AFTER => {
1094 let node_id = reader.read_u32()?;
1095 let (new_tree, _) =
1096 read_mdast_payload(&mut reader, parse_markdown, &arena, node_id)?;
1097 patches.push(Patch::InsertAfter { node_id, new_tree });
1098 }
1099
1100 CMD_PREPEND_CHILD => {
1101 let node_id = reader.read_u32()?;
1102 let (child_tree, _) =
1103 read_mdast_payload(&mut reader, parse_markdown, &arena, node_id)?;
1104 patches.push(Patch::PrependChild {
1105 node_id,
1106 child_tree,
1107 });
1108 }
1109
1110 CMD_APPEND_CHILD => {
1111 let node_id = reader.read_u32()?;
1112 let (child_tree, _) =
1113 read_mdast_payload(&mut reader, parse_markdown, &arena, node_id)?;
1114 patches.push(Patch::AppendChild {
1115 node_id,
1116 child_tree,
1117 });
1118 }
1119
1120 CMD_WRAP => {
1121 let node_id = reader.read_u32()?;
1122 let (parent_tree, _) =
1123 read_mdast_payload(&mut reader, parse_markdown, &arena, node_id)?;
1124 patches.push(Patch::Wrap {
1125 node_id,
1126 parent_tree,
1127 });
1128 }
1129
1130 CMD_REPLACE => {
1131 let node_id = reader.read_u32()?;
1132 let (new_tree, keep_children) =
1133 read_mdast_payload(&mut reader, parse_markdown, &arena, node_id)?;
1134 patches.push(Patch::Replace {
1135 node_id,
1136 new_tree,
1137 keep_children,
1138 });
1139 }
1140
1141 CMD_SET_CHILDREN => {
1142 let node_id = reader.read_u32()?;
1143 let (new_children, _) =
1144 read_mdast_payload(&mut reader, parse_markdown, &arena, node_id)?;
1145 patches.push(Patch::SetChildren {
1146 node_id,
1147 new_children,
1148 });
1149 }
1150
1151 other => return Err(CommandError::UnknownCommand(other)),
1152 }
1153 }
1154
1155 if patches.is_empty() {
1156 Ok((arena, Vec::new()))
1157 } else {
1158 let result = satteri_ast::rebuild::rebuild_lenient(&arena, &patches)?;
1159 Ok((result.arena, result.dropped))
1160 }
1161}
1162
1163pub fn apply_hast_commands(
1181 arena: Arena<Hast>,
1182 command_buf: &[u8],
1183) -> Result<Arena<Hast>, CommandError> {
1184 let (arena, dropped) = apply_hast_commands_lenient(arena, command_buf)?;
1185 if let Some(anchor) = dropped.first() {
1186 return Err(CommandError::PatchOnRemovedSubtree(*anchor));
1187 }
1188 Ok(arena)
1189}
1190
1191pub fn apply_hast_commands_lenient(
1197 mut arena: Arena<Hast>,
1198 command_buf: &[u8],
1199) -> Result<(Arena<Hast>, Vec<u32>), CommandError> {
1200 if command_buf.is_empty() {
1201 return Ok((arena, Vec::new()));
1202 }
1203
1204 let mut patches: Vec<Patch<Hast>> = Vec::new();
1205 let mut reader = BufReader::new(command_buf);
1206
1207 while reader.remaining() > 0 {
1208 let cmd = reader.read_u8()?;
1209
1210 match cmd {
1211 CMD_REMOVE => {
1212 let node_id = reader.read_u32()?;
1213 patches.push(Patch::Remove { node_id });
1214 }
1215
1216 CMD_SET_PROPERTY => {
1217 let node_id = reader.read_u32()?;
1218 let value_type = reader.read_u8()?;
1219 let name_len = reader.read_u32()? as usize;
1220 let name = reader.read_str(name_len)?;
1221 let value_len = reader.read_u32()? as usize;
1222 let value = reader.read_str(value_len)?;
1223 apply_hast_set_property(&mut arena, node_id, name, value_type, value)?;
1224 }
1225
1226 CMD_INSERT_BEFORE => {
1227 let node_id = reader.read_u32()?;
1228 let (new_tree, _) = read_hast_payload(&mut reader, &arena, node_id)?;
1229 patches.push(Patch::InsertBefore { node_id, new_tree });
1230 }
1231
1232 CMD_INSERT_AFTER => {
1233 let node_id = reader.read_u32()?;
1234 let (new_tree, _) = read_hast_payload(&mut reader, &arena, node_id)?;
1235 patches.push(Patch::InsertAfter { node_id, new_tree });
1236 }
1237
1238 CMD_PREPEND_CHILD => {
1239 let node_id = reader.read_u32()?;
1240 let (child_tree, _) = read_hast_payload(&mut reader, &arena, node_id)?;
1241 patches.push(Patch::PrependChild {
1242 node_id,
1243 child_tree,
1244 });
1245 }
1246
1247 CMD_APPEND_CHILD => {
1248 let node_id = reader.read_u32()?;
1249 let (child_tree, _) = read_hast_payload(&mut reader, &arena, node_id)?;
1250 patches.push(Patch::AppendChild {
1251 node_id,
1252 child_tree,
1253 });
1254 }
1255
1256 CMD_WRAP => {
1257 let node_id = reader.read_u32()?;
1258 let (parent_tree, _) = read_hast_payload(&mut reader, &arena, node_id)?;
1259 patches.push(Patch::Wrap {
1260 node_id,
1261 parent_tree,
1262 });
1263 }
1264
1265 CMD_REPLACE => {
1266 let node_id = reader.read_u32()?;
1267 let (new_tree, keep_children) = read_hast_payload(&mut reader, &arena, node_id)?;
1268 patches.push(Patch::Replace {
1269 node_id,
1270 new_tree,
1271 keep_children,
1272 });
1273 }
1274
1275 CMD_SET_CHILDREN => {
1276 let node_id = reader.read_u32()?;
1277 let (new_children, _) = read_hast_payload(&mut reader, &arena, node_id)?;
1278 patches.push(Patch::SetChildren {
1279 node_id,
1280 new_children,
1281 });
1282 }
1283
1284 other => return Err(CommandError::UnknownCommand(other)),
1285 }
1286 }
1287
1288 if patches.is_empty() {
1289 Ok((arena, Vec::new()))
1290 } else {
1291 let result = satteri_ast::rebuild::rebuild_lenient(&arena, &patches)?;
1292 Ok((result.arena, result.dropped))
1293 }
1294}
1295
1296#[cfg(test)]
1297mod tests {
1298 use super::*;
1299 use satteri_ast::shared::PROP_INT;
1300
1301 fn op_open(b: &mut Vec<u8>, t: MdastNodeType) {
1302 b.push(OP_OPEN);
1303 b.push(t as u8);
1304 }
1305 fn op_close(b: &mut Vec<u8>) {
1306 b.push(OP_CLOSE);
1307 }
1308 fn op_str(b: &mut Vec<u8>, field: u8, s: &str) {
1309 b.push(OP_STR);
1310 b.push(field);
1311 b.extend_from_slice(&(s.len() as u32).to_le_bytes());
1312 b.extend_from_slice(s.as_bytes());
1313 }
1314 fn op_u8(b: &mut Vec<u8>, field: u8, v: u8) {
1315 b.push(OP_U8);
1316 b.push(field);
1317 b.push(v);
1318 }
1319
1320 #[test]
1321 fn opstream_replay_builds_subtree() {
1322 let mut ops = Vec::new();
1324 op_open(&mut ops, MdastNodeType::Blockquote);
1325 op_open(&mut ops, MdastNodeType::Heading);
1326 op_u8(&mut ops, OF_DEPTH, 3);
1327 op_open(&mut ops, MdastNodeType::Text);
1328 op_str(&mut ops, OF_VALUE, "Note");
1329 op_close(&mut ops);
1330 op_close(&mut ops);
1331 op_open(&mut ops, MdastNodeType::Paragraph);
1332 op_open(&mut ops, MdastNodeType::Text);
1333 op_str(&mut ops, OF_VALUE, "Body");
1334 op_close(&mut ops);
1335 op_close(&mut ops);
1336 op_close(&mut ops);
1337
1338 let empty = ArenaBuilder::<Mdast>::new(String::new()).finish();
1339 let arena = replay_mdast_opstream(&ops, &empty, 0).unwrap();
1340
1341 assert_eq!(arena.get_node(0).node_type, MdastNodeType::Blockquote as u8);
1343 let top = arena.get_children(0).to_vec();
1344 assert_eq!(top.len(), 2);
1345 let h = top[0];
1347 assert_eq!(arena.get_node(h).node_type, MdastNodeType::Heading as u8);
1348 assert_eq!(decode_heading_data(arena.get_type_data(h)).depth, 3);
1349 let h_text = arena.get_children(h)[0];
1350 assert_eq!(arena.get_node(h_text).node_type, MdastNodeType::Text as u8);
1351 let sref = decode_string_ref_data(arena.get_type_data(h_text));
1352 assert_eq!(arena.get_str(sref), "Note");
1353 let p = top[1];
1355 assert_eq!(arena.get_node(p).node_type, MdastNodeType::Paragraph as u8);
1356 let p_text = arena.get_children(p)[0];
1357 assert_eq!(
1358 arena.get_str(decode_string_ref_data(arena.get_type_data(p_text))),
1359 "Body"
1360 );
1361 }
1362
1363 #[test]
1364 fn opstream_replay_rejects_unbalanced_close() {
1365 let empty = ArenaBuilder::<Mdast>::new(String::new()).finish();
1366 let err = replay_mdast_opstream(&[OP_CLOSE], &empty, 0).unwrap_err();
1367 assert!(matches!(err, CommandError::UnbalancedOpstream));
1368
1369 let empty_hast = ArenaBuilder::<Hast>::new(String::new()).finish();
1370 let err = replay_hast_opstream(&[OP_CLOSE], &empty_hast, 0).unwrap_err();
1371 assert!(matches!(err, CommandError::UnbalancedOpstream));
1372
1373 let mut ops = Vec::new();
1375 op_open(&mut ops, MdastNodeType::Paragraph);
1376 op_close(&mut ops);
1377 op_close(&mut ops);
1378 let err = replay_mdast_opstream(&ops, &empty, 0).unwrap_err();
1379 assert!(matches!(err, CommandError::UnbalancedOpstream));
1380 }
1381
1382 #[test]
1383 fn opstream_replay_rejects_unclosed_node() {
1384 let mut ops = Vec::new();
1387 op_open(&mut ops, MdastNodeType::Heading);
1388 op_u8(&mut ops, OF_DEPTH, 2);
1389
1390 let empty = ArenaBuilder::<Mdast>::new(String::new()).finish();
1391 let err = replay_mdast_opstream(&ops, &empty, 0).unwrap_err();
1392 assert!(matches!(err, CommandError::UnbalancedOpstream));
1393
1394 let hast_ops = vec![OP_OPEN, HastNodeType::Element as u8];
1395 let empty_hast = ArenaBuilder::<Hast>::new(String::new()).finish();
1396 let err = replay_hast_opstream(&hast_ops, &empty_hast, 0).unwrap_err();
1397 assert!(matches!(err, CommandError::UnbalancedOpstream));
1398 }
1399
1400 #[test]
1401 fn opstream_keep_children_rejects_out_of_range_anchor() {
1402 let orig = test_parse_markdown("Hello");
1403 let bad_anchor = orig.len() as u32;
1404
1405 let mut ops = Vec::new();
1406 op_open(&mut ops, MdastNodeType::Heading);
1407 ops.push(OP_KEEP_CHILDREN);
1408 op_close(&mut ops);
1409
1410 let err = replay_mdast_opstream(&ops, &orig, bad_anchor).unwrap_err();
1411 assert!(matches!(err, CommandError::InvalidNodeId(id) if id == bad_anchor));
1412 }
1413
1414 #[test]
1415 fn set_property_rejects_out_of_range_node_id() {
1416 let arena = build_hello_world();
1417 let bad_id = arena.len() as u32;
1418 let mut buf = Vec::new();
1419 push_set_property(&mut buf, bad_id, PROP_INT, "depth", "3");
1420 let err = apply_mdast_commands(arena, &buf, &test_parse_markdown).unwrap_err();
1421 assert!(matches!(err, CommandError::InvalidNodeId(id) if id == bad_id));
1422
1423 let hast = build_hast_element(&[]);
1424 let bad_id = hast.len() as u32;
1425 let mut buf = Vec::new();
1426 push_set_property(&mut buf, bad_id, PROP_STRING, "class", "x");
1427 let err = apply_hast_commands(hast, &buf).unwrap_err();
1428 assert!(matches!(err, CommandError::InvalidNodeId(id) if id == bad_id));
1429 }
1430
1431 #[test]
1432 fn opstream_replay_rejects_unknown_tags() {
1433 let empty = ArenaBuilder::<Mdast>::new(String::new()).finish();
1434 let err = replay_mdast_opstream(&[OP_OPEN, 200], &empty, 0).unwrap_err();
1435 assert!(matches!(err, CommandError::UnknownNodeType(_)));
1436
1437 let empty_hast = ArenaBuilder::<Hast>::new(String::new()).finish();
1438 let err = replay_hast_opstream(&[OP_OPEN, 200], &empty_hast, 0).unwrap_err();
1439 assert!(matches!(err, CommandError::UnknownNodeType(_)));
1440 }
1441
1442 #[test]
1443 fn opstream_keep_children_splices_original_children() {
1444 let orig = test_parse_markdown("Hello");
1447 let para = orig.get_children(0)[0];
1448 let orig_text = orig.get_children(para)[0];
1449
1450 let mut ops = Vec::new();
1451 op_open(&mut ops, MdastNodeType::Heading);
1452 op_u8(&mut ops, OF_DEPTH, 2);
1453 ops.push(OP_KEEP_CHILDREN);
1454 op_close(&mut ops);
1455
1456 let arena = replay_mdast_opstream(&ops, &orig, para).unwrap();
1457 assert_eq!(arena.get_node(0).node_type, MdastNodeType::Heading as u8);
1458 assert_eq!(decode_heading_data(arena.get_type_data(0)).depth, 2);
1459 let children = arena.get_children(0).to_vec();
1460 assert_eq!(children.len(), 1);
1461 assert_eq!(arena.get_node(children[0]).node_type, REF_NODE_TYPE);
1462 assert_eq!(
1463 u32::from_le_bytes(arena.get_type_data(children[0]).try_into().unwrap()),
1464 orig_text
1465 );
1466 }
1467
1468 #[test]
1469 fn opstream_ref_rejects_out_of_range_id() {
1470 let orig = test_parse_markdown("Hello");
1473 let bad = orig.len() as u32 + 100;
1474
1475 let mut ops = Vec::new();
1476 op_open(&mut ops, MdastNodeType::Paragraph);
1477 ops.push(OP_REF);
1478 ops.extend_from_slice(&bad.to_le_bytes());
1479 op_close(&mut ops);
1480
1481 let err = replay_mdast_opstream(&ops, &orig, 0).unwrap_err();
1482 assert!(matches!(err, CommandError::InvalidNodeId(id) if id == bad));
1483 }
1484
1485 #[test]
1486 fn opstream_rejects_nested_root() {
1487 let empty = ArenaBuilder::<Mdast>::new(String::new()).finish();
1488
1489 let mut ok = Vec::new();
1491 op_open(&mut ok, MdastNodeType::Root);
1492 op_close(&mut ok);
1493 assert!(replay_mdast_opstream(&ok, &empty, 0).is_ok());
1494
1495 let mut ops = Vec::new();
1496 op_open(&mut ops, MdastNodeType::Root);
1497 op_open(&mut ops, MdastNodeType::Root);
1498 let err = replay_mdast_opstream(&ops, &empty, 0).unwrap_err();
1499 assert!(matches!(err, CommandError::UnencodableNodeType("root")));
1500 }
1501
1502 #[test]
1503 fn hast_opstream_rejects_doctype() {
1504 let empty = ArenaBuilder::<Hast>::new(String::new()).finish();
1505 let ops = vec![OP_OPEN, HastNodeType::Doctype as u8, OP_CLOSE];
1506 let err = replay_hast_opstream(&ops, &empty, 0).unwrap_err();
1507 assert!(matches!(err, CommandError::UnencodableNodeType("doctype")));
1508 }
1509
1510 #[test]
1511 fn opstream_rejects_over_deep_nesting() {
1512 let empty = ArenaBuilder::<Mdast>::new(String::new()).finish();
1515 let mut ops = Vec::new();
1516 for _ in 0..(MAX_OPSTREAM_DEPTH + 1) {
1517 op_open(&mut ops, MdastNodeType::Blockquote);
1518 }
1519 let err = replay_mdast_opstream(&ops, &empty, 0).unwrap_err();
1520 assert!(matches!(err, CommandError::OpstreamTooDeep(_)));
1521 }
1522
1523 #[test]
1524 fn set_property_rejects_out_of_range_or_unparseable_int() {
1525 let heading_id = 1;
1527
1528 let mut buf = Vec::new();
1529 push_set_property(&mut buf, heading_id, PROP_INT, "depth", "9999");
1530 let err =
1531 apply_mdast_commands(build_hello_world(), &buf, &test_parse_markdown).unwrap_err();
1532 assert!(matches!(err, CommandError::PropertyValueOutOfRange { .. }));
1533
1534 let mut buf = Vec::new();
1535 push_set_property(&mut buf, heading_id, PROP_INT, "depth", "not-a-number");
1536 let err =
1537 apply_mdast_commands(build_hello_world(), &buf, &test_parse_markdown).unwrap_err();
1538 assert!(matches!(err, CommandError::PropertyValueOutOfRange { .. }));
1539
1540 let mut buf = Vec::new();
1542 push_set_property(&mut buf, heading_id, PROP_INT, "depth", "255");
1543 let arena = apply_mdast_commands(build_hello_world(), &buf, &test_parse_markdown).unwrap();
1544 assert_eq!(
1545 decode_heading_data(arena.get_type_data(heading_id)).depth,
1546 255
1547 );
1548 }
1549
1550 #[cfg(feature = "mdx")]
1551 #[test]
1552 fn mdx_jsx_set_property_replaces_named_attrs_and_keeps_explicit() {
1553 use satteri_ast::shared::MDX_ATTR_EXPRESSION_PROP;
1554
1555 let mut b = ArenaBuilder::<Hast>::new(String::new());
1556 b.open_node(HastNodeType::Root as u8);
1557 b.open_node(HastNodeType::MdxJsxElement as u8);
1558 let elem_name = b.alloc_string("Box");
1559 let foo = b.alloc_string("foo");
1560 let expr = b.alloc_string("1+1");
1561 let rest = b.alloc_string("rest");
1562 let attrs = vec![
1563 (MDX_ATTR_EXPRESSION_PROP, foo, expr),
1564 (MDX_ATTR_SPREAD, StringRef::empty(), rest),
1565 ];
1566 b.set_data_current(&encode_mdx_jsx_element_data(elem_name, &attrs, true));
1567 b.close_node();
1568 b.close_node();
1569 let mut arena = b.finish();
1570
1571 apply_hast_mdx_jsx_attribute(&mut arena, 1, "foo", PROP_STRING, "x").unwrap();
1574 let data = arena.get_type_data(1).to_vec();
1575 assert_eq!(decode_mdx_jsx_attr_count(&data), 2);
1576 assert!(decode_mdx_jsx_explicit(&data));
1577 let (k0, _, _) = decode_mdx_jsx_attr(&data, 0);
1578 assert_eq!(k0, MDX_ATTR_SPREAD);
1579 let (k1, n1, v1) = decode_mdx_jsx_attr(&data, 1);
1580 assert_eq!(k1, MDX_ATTR_LITERAL_PROP);
1581 assert_eq!(arena.get_str(n1), "foo");
1582 assert_eq!(arena.get_str(v1), "x");
1583
1584 apply_hast_mdx_jsx_attribute(&mut arena, 1, "id", PROP_STRING, "intro").unwrap();
1586 let data = arena.get_type_data(1).to_vec();
1587 assert_eq!(decode_mdx_jsx_attr_count(&data), 3);
1588 assert!(decode_mdx_jsx_explicit(&data));
1589 }
1590
1591 fn test_parse_markdown(source: &str) -> Arena<Mdast> {
1592 let mut b = ArenaBuilder::<Mdast>::new(String::new());
1593 b.open_node(MdastNodeType::Root as u8);
1594 b.open_node(MdastNodeType::Paragraph as u8);
1595 b.open_node(MdastNodeType::Text as u8);
1596 let sref = b.alloc_string(source);
1597 b.set_data_current(&satteri_arena::encode_string_ref_data(sref));
1598 b.close_node();
1599 b.close_node();
1600 b.close_node();
1601 b.finish()
1602 }
1603
1604 fn push_u32(buf: &mut Vec<u8>, v: u32) {
1605 buf.extend_from_slice(&v.to_le_bytes());
1606 }
1607
1608 fn push_set_property(buf: &mut Vec<u8>, node_id: u32, value_type: u8, name: &str, value: &str) {
1610 buf.push(CMD_SET_PROPERTY);
1611 push_u32(buf, node_id);
1612 buf.push(value_type);
1613 push_u32(buf, name.len() as u32);
1614 buf.extend_from_slice(name.as_bytes());
1615 push_u32(buf, value.len() as u32);
1616 buf.extend_from_slice(value.as_bytes());
1617 }
1618
1619 fn build_hello_world() -> Arena<Mdast> {
1620 use satteri_ast::mdast::codec::{encode_heading_data, encode_string_ref_data};
1621
1622 let source = "# Hello\n\nWorld".to_string();
1623 let mut b = ArenaBuilder::<Mdast>::new(source);
1624
1625 b.open_node(MdastNodeType::Root as u8);
1626 b.set_position_current(0, 14, 1, 1, 2, 6);
1627
1628 b.open_node(MdastNodeType::Heading as u8);
1629 b.set_position_current(0, 7, 1, 1, 1, 8);
1630 b.set_data_current(&encode_heading_data(1));
1631
1632 b.open_node(MdastNodeType::Text as u8);
1633 b.set_position_current(2, 7, 1, 3, 1, 8);
1634 b.set_data_current(&encode_string_ref_data(StringRef::new(2, 5)));
1635 b.close_node();
1636
1637 b.close_node();
1638
1639 b.open_node(MdastNodeType::Paragraph as u8);
1640 b.set_position_current(9, 14, 2, 1, 2, 6);
1641
1642 b.open_node(MdastNodeType::Text as u8);
1643 b.set_position_current(9, 14, 2, 1, 2, 6);
1644 b.set_data_current(&encode_string_ref_data(StringRef::new(9, 5)));
1645 b.close_node();
1646
1647 b.close_node();
1648 b.close_node();
1649
1650 b.finish()
1651 }
1652
1653 #[test]
1654 fn empty_command_buffer() {
1655 let arena = build_hello_world();
1656 let result = apply_mdast_commands(arena.clone(), &[], &test_parse_markdown).unwrap();
1657 assert_eq!(result.len(), arena.len());
1658 }
1659
1660 #[test]
1661 fn remove_command() {
1662 let arena = build_hello_world();
1663 let heading_id = arena.get_children(0)[0];
1664 let mut buf = Vec::new();
1665 buf.push(CMD_REMOVE);
1666 push_u32(&mut buf, heading_id);
1667
1668 let result = apply_mdast_commands(arena.clone(), &buf, &test_parse_markdown).unwrap();
1669 assert_eq!(result.get_children(0).len(), 1);
1670 assert_eq!(
1671 result.get_node(result.get_children(0)[0]).node_type,
1672 MdastNodeType::Paragraph as u8
1673 );
1674 }
1675
1676 #[test]
1677 fn set_property_heading_depth() {
1678 let arena = build_hello_world();
1679 let heading_id = arena.get_children(0)[0];
1680
1681 let mut buf = Vec::new();
1682 push_set_property(&mut buf, heading_id, PROP_INT, "depth", "3");
1683
1684 let result = apply_mdast_commands(arena.clone(), &buf, &test_parse_markdown).unwrap();
1685 let heading_data = result.get_type_data(heading_id);
1686 let heading = decode_heading_data(heading_data);
1687 assert_eq!(heading.depth, 3);
1688 }
1689
1690 #[test]
1691 fn set_property_text_value() {
1692 let arena = build_hello_world();
1693 let heading_id = arena.get_children(0)[0];
1694 let text_id = arena.get_children(heading_id)[0];
1695
1696 let mut buf = Vec::new();
1697 push_set_property(&mut buf, text_id, PROP_STRING, "value", "Goodbye");
1698
1699 let result = apply_mdast_commands(arena.clone(), &buf, &test_parse_markdown).unwrap();
1700 let text_data = result.get_type_data(text_id);
1701 let sref = decode_string_ref_data(text_data);
1702 assert_eq!(result.get_str(sref), "Goodbye");
1703 }
1704
1705 #[test]
1706 fn replace_with_raw_markdown() {
1707 let arena = build_hello_world();
1708 let heading_id = arena.get_children(0)[0];
1709
1710 let raw_md = "## New Heading";
1711 let mut buf = Vec::new();
1712 buf.push(CMD_REPLACE);
1713 push_u32(&mut buf, heading_id);
1714 buf.push(PAYLOAD_RAW_MARKDOWN);
1715 push_u32(&mut buf, raw_md.len() as u32);
1716 buf.extend_from_slice(raw_md.as_bytes());
1717
1718 let result = apply_mdast_commands(arena.clone(), &buf, &test_parse_markdown).unwrap();
1719 let root_children = result.get_children(0);
1720 assert!(root_children.len() >= 2);
1721 }
1722
1723 #[test]
1724 fn multiple_commands() {
1725 let arena = build_hello_world();
1726 let heading_id = arena.get_children(0)[0];
1727 let text_id = arena.get_children(heading_id)[0];
1728
1729 let mut buf = Vec::new();
1730 push_set_property(&mut buf, heading_id, PROP_INT, "depth", "3");
1731 push_set_property(&mut buf, text_id, PROP_STRING, "value", "Hi");
1732
1733 let result = apply_mdast_commands(arena.clone(), &buf, &test_parse_markdown).unwrap();
1734
1735 let heading_data = result.get_type_data(heading_id);
1736 assert_eq!(decode_heading_data(heading_data).depth, 3);
1737
1738 let text_data = result.get_type_data(text_id);
1739 let sref = decode_string_ref_data(text_data);
1740 assert_eq!(result.get_str(sref), "Hi");
1741 }
1742
1743 #[test]
1744 fn set_property_null() {
1745 let arena = build_hello_world();
1746 let heading_id = arena.get_children(0)[0];
1747 let text_id = arena.get_children(heading_id)[0];
1748
1749 let mut buf = Vec::new();
1750 push_set_property(&mut buf, text_id, PROP_NULL, "value", "");
1751
1752 let result = apply_mdast_commands(arena.clone(), &buf, &test_parse_markdown).unwrap();
1753 let text_data = result.get_type_data(text_id);
1754 let sref = decode_string_ref_data(text_data);
1755 assert_eq!(sref.len, 0);
1756 }
1757
1758 #[test]
1759 fn set_property_invalid_field_reports_property_and_node_type() {
1760 let arena = build_hello_world();
1761 let heading_id = arena.get_children(0)[0];
1762
1763 let mut buf = Vec::new();
1764 push_set_property(&mut buf, heading_id, PROP_STRING, "value", "x");
1765
1766 let err = apply_mdast_commands(arena, &buf, &test_parse_markdown).unwrap_err();
1767 assert!(matches!(
1768 err,
1769 CommandError::UnknownField { ref name, ref node_type }
1770 if name == "value" && node_type == "heading"
1771 ));
1772 assert_eq!(
1773 err.to_string(),
1774 "cannot set property 'value' on a 'heading' node"
1775 );
1776 }
1777
1778 #[test]
1779 fn set_property_wrong_value_type_reports_value_mismatch() {
1780 let arena = build_hello_world();
1781 let heading_id = arena.get_children(0)[0];
1782
1783 let mut buf = Vec::new();
1785 push_set_property(&mut buf, heading_id, PROP_STRING, "depth", "3");
1786
1787 let err = apply_mdast_commands(arena, &buf, &test_parse_markdown).unwrap_err();
1788 assert!(matches!(
1789 err,
1790 CommandError::InvalidPropertyValue { ref name, ref node_type }
1791 if name == "depth" && node_type == "heading"
1792 ));
1793 assert_eq!(
1794 err.to_string(),
1795 "property 'depth' on a 'heading' node cannot hold a value of this type"
1796 );
1797 }
1798
1799 fn build_single_node(node_type: MdastNodeType, type_data: &[u8]) -> Arena<Mdast> {
1801 let mut b = ArenaBuilder::<Mdast>::new(String::new());
1802 b.open_node(MdastNodeType::Root as u8);
1803 b.open_node(node_type as u8);
1804 b.set_data_current(type_data);
1805 b.close_node();
1806 b.close_node();
1807 b.finish()
1808 }
1809
1810 #[test]
1811 fn set_property_image_reference_alt_roundtrip() {
1812 let mut b = ArenaBuilder::<Mdast>::new(String::new());
1813 b.open_node(MdastNodeType::Root as u8);
1814 b.open_node(MdastNodeType::ImageReference as u8);
1815 let identifier = b.alloc_string("img");
1816 let alt = b.alloc_string("old");
1817 b.set_data_current(&encode_image_reference_data(identifier, identifier, 0, alt));
1818 b.close_node();
1819 b.close_node();
1820 let arena = b.finish();
1821 let image_ref_id = arena.get_children(0)[0];
1822
1823 let mut buf = Vec::new();
1824 push_set_property(&mut buf, image_ref_id, PROP_STRING, "alt", "new alt");
1825
1826 let result = apply_mdast_commands(arena, &buf, &test_parse_markdown).unwrap();
1827 let alt = decode_image_reference_alt(result.get_type_data(image_ref_id));
1828 assert_eq!(result.get_str(alt), "new alt");
1829 }
1830
1831 #[test]
1832 fn set_property_reference_type_valid_and_invalid() {
1833 let mut b = ArenaBuilder::<Mdast>::new(String::new());
1834 b.open_node(MdastNodeType::Root as u8);
1835 b.open_node(MdastNodeType::LinkReference as u8);
1836 let identifier = b.alloc_string("ref");
1837 b.set_data_current(&encode_reference_data(identifier, identifier, 0));
1838 b.close_node();
1839 b.close_node();
1840 let arena = b.finish();
1841 let link_ref_id = arena.get_children(0)[0];
1842
1843 let mut buf = Vec::new();
1844 push_set_property(&mut buf, link_ref_id, PROP_STRING, "referenceType", "full");
1845 let result = apply_mdast_commands(arena.clone(), &buf, &test_parse_markdown).unwrap();
1846 let reference = decode_reference_data(result.get_type_data(link_ref_id));
1847 assert_eq!(reference.reference_kind, 2);
1848
1849 let mut buf = Vec::new();
1851 push_set_property(&mut buf, link_ref_id, PROP_STRING, "referenceType", "bogus");
1852 let err = apply_mdast_commands(arena, &buf, &test_parse_markdown).unwrap_err();
1853 assert!(matches!(
1854 err,
1855 CommandError::InvalidPropertyValue { ref name, ref node_type }
1856 if name == "referenceType" && node_type == "linkReference"
1857 ));
1858 }
1859
1860 #[test]
1861 fn set_property_list_start_and_ordered() {
1862 let arena = build_single_node(MdastNodeType::List, &encode_list_data(false, 1, false));
1863 let list_id = arena.get_children(0)[0];
1864
1865 let mut buf = Vec::new();
1866 push_set_property(&mut buf, list_id, PROP_INT, "start", "5");
1867 push_set_property(&mut buf, list_id, PROP_BOOL_TRUE, "ordered", "");
1868
1869 let result = apply_mdast_commands(arena, &buf, &test_parse_markdown).unwrap();
1870 let list = decode_list_data(result.get_type_data(list_id));
1871 assert_eq!(list.start, 5);
1872 assert!(list.ordered);
1873 assert!(!list.spread);
1874 }
1875
1876 #[test]
1877 fn escape_braces_in_html_text_basic() {
1878 assert_eq!(
1879 escape_braces_in_html_text("<span>{foo: 1}</span>"),
1880 "<span>{'{'}foo: 1{'}'}</span>"
1881 );
1882 }
1883
1884 #[test]
1885 fn escape_braces_preserves_attributes() {
1886 let result = escape_braces_in_html_text(r#"<span data-x="{a}">{b}</span>"#);
1887 assert!(
1888 result.contains(r#"data-x="{a}""#),
1889 "attribute braces preserved"
1890 );
1891 assert!(result.contains("{'{'}"), "text braces escaped");
1892 }
1893
1894 #[test]
1895 fn escape_braces_no_braces() {
1896 let html = r#"<pre class="shiki"><code><span style="color:red">hello</span></code></pre>"#;
1897 assert_eq!(escape_braces_in_html_text(html), html);
1898 }
1899
1900 #[test]
1901 fn escape_braces_shiki_output() {
1902 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>"#;
1903 let escaped = escape_braces_in_html_text(html);
1904 assert!(
1905 !escaped.contains(">{<"),
1906 "bare braces in text should be escaped"
1907 );
1908 assert!(
1909 !escaped.contains(">}<"),
1910 "bare braces in text should be escaped"
1911 );
1912 assert!(escaped.contains(r#"class="shiki""#));
1913 assert!(escaped.contains(r#"style="color:#E1E4E8""#));
1914 }
1915
1916 #[test]
1917 fn hast_set_property_add_new() {
1918 let arena = build_hast_element(&[]);
1919 let element_id = arena.get_children(0)[0];
1920
1921 let mut buf = Vec::new();
1922 push_set_property(&mut buf, element_id, PROP_STRING, "class", "test");
1923
1924 let result = apply_hast_commands(arena.clone(), &buf).unwrap();
1925 let data = result.get_type_data(element_id);
1926 let prop_count = u32::from_le_bytes(data[8..12].try_into().unwrap());
1927 assert_eq!(prop_count, 1);
1928 let name_ref = StringRef::new(
1929 u32::from_le_bytes(data[16..20].try_into().unwrap()),
1930 u32::from_le_bytes(data[20..24].try_into().unwrap()),
1931 );
1932 assert_eq!(result.get_str(name_ref), "class");
1933 let val_ref = StringRef::new(
1934 u32::from_le_bytes(data[28..32].try_into().unwrap()),
1935 u32::from_le_bytes(data[32..36].try_into().unwrap()),
1936 );
1937 assert_eq!(result.get_str(val_ref), "test");
1938 assert_eq!(data[24], PROP_STRING);
1939 }
1940
1941 #[test]
1942 fn hast_set_property_overwrite_existing() {
1943 let arena = build_hast_element(&[("class", PROP_STRING, "old")]);
1944 let element_id = arena.get_children(0)[0];
1945
1946 let mut buf = Vec::new();
1947 push_set_property(&mut buf, element_id, PROP_STRING, "class", "new-value");
1948
1949 let result = apply_hast_commands(arena.clone(), &buf).unwrap();
1950 let data = result.get_type_data(element_id);
1951 let prop_count = u32::from_le_bytes(data[8..12].try_into().unwrap());
1952 assert_eq!(prop_count, 1);
1953 let val_ref = StringRef::new(
1954 u32::from_le_bytes(data[28..32].try_into().unwrap()),
1955 u32::from_le_bytes(data[32..36].try_into().unwrap()),
1956 );
1957 assert_eq!(result.get_str(val_ref), "new-value");
1958 }
1959
1960 #[test]
1961 fn hast_set_property_bool_true() {
1962 let arena = build_hast_element(&[]);
1963 let element_id = arena.get_children(0)[0];
1964
1965 let mut buf = Vec::new();
1966 push_set_property(&mut buf, element_id, PROP_BOOL_TRUE, "disabled", "");
1967
1968 let result = apply_hast_commands(arena.clone(), &buf).unwrap();
1969 let data = result.get_type_data(element_id);
1970 let prop_count = u32::from_le_bytes(data[8..12].try_into().unwrap());
1971 assert_eq!(prop_count, 1);
1972 assert_eq!(data[24], PROP_BOOL_TRUE);
1973 }
1974
1975 #[test]
1976 fn hast_set_property_multiple_on_same_node() {
1977 let arena = build_hast_element(&[]);
1978 let element_id = arena.get_children(0)[0];
1979
1980 let mut buf = Vec::new();
1981 push_set_property(&mut buf, element_id, PROP_STRING, "class", "foo");
1982 push_set_property(&mut buf, element_id, PROP_STRING, "id", "bar");
1983
1984 let result = apply_hast_commands(arena.clone(), &buf).unwrap();
1985 let data = result.get_type_data(element_id);
1986 let prop_count = u32::from_le_bytes(data[8..12].try_into().unwrap());
1987 assert_eq!(prop_count, 2);
1988 }
1989
1990 fn build_hast_element(props: &[(&str, u8, &str)]) -> Arena<Hast> {
1992 use satteri_ast::hast::node::HastNodeType;
1993
1994 let mut b = ArenaBuilder::<Hast>::new(String::new());
1995 b.open_node_raw(HastNodeType::Root as u8);
1996 b.open_node_raw(HastNodeType::Element as u8);
1997 let tag_ref = b.alloc_string("div");
1998 let prop_tuples: Vec<(StringRef, u8, StringRef)> = props
1999 .iter()
2000 .map(|(name, kind, value)| {
2001 let n = b.alloc_string(name);
2002 let v = if value.is_empty() {
2003 StringRef::empty()
2004 } else {
2005 b.alloc_string(value)
2006 };
2007 (n, *kind, v)
2008 })
2009 .collect();
2010 let mut type_data = Vec::with_capacity(16 + prop_tuples.len() * 20);
2011 type_data.extend_from_slice(&tag_ref.offset.to_le_bytes());
2012 type_data.extend_from_slice(&tag_ref.len.to_le_bytes());
2013 type_data.extend_from_slice(&(prop_tuples.len() as u32).to_le_bytes());
2014 type_data.extend_from_slice(&0u32.to_le_bytes());
2015 for (n, kind, v) in &prop_tuples {
2016 type_data.extend_from_slice(&n.offset.to_le_bytes());
2017 type_data.extend_from_slice(&n.len.to_le_bytes());
2018 type_data.push(*kind);
2019 type_data.extend_from_slice(&[0u8; 3]);
2020 type_data.extend_from_slice(&v.offset.to_le_bytes());
2021 type_data.extend_from_slice(&v.len.to_le_bytes());
2022 }
2023 b.set_data_current(&type_data);
2024 b.close_node();
2025 b.close_node();
2026 b.finish()
2027 }
2028}