1use satteri_arena::{Arena, ArenaBuilder, ArenaKind, Hast, Mdast, StringRef};
24use satteri_ast::commands::CommandError;
25use satteri_ast::hast::codec::decode_element_tag;
26use satteri_ast::hast::{HastNodeType, is_void_element};
27use satteri_ast::mdast::MdastNodeType;
28use satteri_ast::mdast::codec::*;
29use satteri_ast::patch::{Patch, PatchContent, REF_NODE_TYPE};
30#[cfg(feature = "mdx")]
31use satteri_ast::shared::{MDX_ATTR_BOOLEAN_PROP, MDX_ATTR_LITERAL_PROP, MDX_ATTR_SPREAD};
32use satteri_ast::shared::{
33 PROP_BOOL_FALSE, PROP_BOOL_TRUE, PROP_INT, PROP_NULL, PROP_SPACE_SEP, PROP_STRING,
34};
35
36use crate::generated::prop_slots::{MdastPropSlot, mdast_prop_slot};
37use crate::generated::wire_constants::*;
38
39struct BufReader<'a> {
40 data: &'a [u8],
41 pos: usize,
42}
43
44impl<'a> BufReader<'a> {
45 fn new(data: &'a [u8]) -> Self {
46 Self { data, pos: 0 }
47 }
48
49 fn remaining(&self) -> usize {
50 self.data.len() - self.pos
51 }
52
53 fn read_u8(&mut self) -> Result<u8, CommandError> {
54 if self.remaining() < 1 {
55 return Err(CommandError::UnexpectedEof);
56 }
57 let v = self.data[self.pos];
58 self.pos += 1;
59 Ok(v)
60 }
61
62 fn read_u32(&mut self) -> Result<u32, CommandError> {
63 if self.remaining() < 4 {
64 return Err(CommandError::UnexpectedEof);
65 }
66 let v = u32::from_le_bytes([
67 self.data[self.pos],
68 self.data[self.pos + 1],
69 self.data[self.pos + 2],
70 self.data[self.pos + 3],
71 ]);
72 self.pos += 4;
73 Ok(v)
74 }
75
76 fn read_anchor(&mut self, original_len: u32) -> Result<u32, CommandError> {
78 let node_id = self.read_u32()?;
79 if node_id >= original_len {
80 return Err(CommandError::InvalidNodeId(node_id));
81 }
82 Ok(node_id)
83 }
84
85 fn read_bytes(&mut self, len: usize) -> Result<&'a [u8], CommandError> {
86 if self.remaining() < len {
87 return Err(CommandError::UnexpectedEof);
88 }
89 let slice = &self.data[self.pos..self.pos + len];
90 self.pos += len;
91 Ok(slice)
92 }
93
94 fn read_str(&mut self, len: usize) -> Result<&'a str, CommandError> {
95 let bytes = self.read_bytes(len)?;
96 std::str::from_utf8(bytes).map_err(|_| CommandError::InvalidUtf8)
97 }
98}
99
100fn apply_data_property<K: ArenaKind>(
103 arena: &mut Arena<K>,
104 node_id: u32,
105 value_type: u8,
106 value_str: &str,
107) {
108 if value_type == PROP_NULL {
109 arena.set_node_data(node_id, Vec::new());
110 } else {
111 arena.set_node_data(node_id, value_str.as_bytes().to_vec());
112 }
113}
114
115fn mdast_type_name(node_type: u8) -> String {
117 match MdastNodeType::from_u8(node_type) {
118 Some(t) => t.name().to_string(),
119 None => format!("unknown({node_type})"),
120 }
121}
122
123fn apply_mdast_set_property(
127 arena: &mut Arena<Mdast>,
128 node_id: u32,
129 prop_name: &str,
130 value_type: u8,
131 value_str: &str,
132) -> Result<(), CommandError> {
133 if node_id as usize >= arena.len() {
134 return Err(CommandError::InvalidNodeId(node_id));
135 }
136 if prop_name == "data" {
137 apply_data_property(arena, node_id, value_type, value_str);
138 return Ok(());
139 }
140
141 let node_type = arena.get_node(node_id).node_type;
142
143 let slot = mdast_prop_slot(node_type, prop_name).ok_or_else(|| CommandError::UnknownField {
145 node_type: mdast_type_name(node_type),
146 name: prop_name.to_string(),
147 })?;
148
149 let written = write_mdast_prop_slot(arena, node_id, slot, prop_name, value_type, value_str)?;
152 written.ok_or_else(|| CommandError::InvalidPropertyValue {
153 node_type: mdast_type_name(node_type),
154 name: prop_name.to_string(),
155 })
156}
157
158fn write_mdast_prop_slot(
162 arena: &mut Arena<Mdast>,
163 node_id: u32,
164 slot: MdastPropSlot,
165 prop_name: &str,
166 value_type: u8,
167 value_str: &str,
168) -> Result<Option<()>, CommandError> {
169 use MdastPropSlot as S;
170 match value_type {
171 PROP_STRING | PROP_SPACE_SEP => match slot {
172 S::Str { offset } => {
173 let sref = arena.alloc_string(value_str);
174 set_mdast_string_ref(arena, node_id, offset, sref)?;
175 }
176 S::Enum8 { offset, values } => {
177 let Some(v) = values.iter().position(|v| *v == value_str) else {
178 return Ok(None);
179 };
180 set_mdast_scalar(arena, node_id, offset, &[v as u8]);
181 }
182 _ => return Ok(None),
183 },
184 PROP_BOOL_TRUE | PROP_BOOL_FALSE => match slot {
185 S::Bool { offset } => {
186 let value = value_type == PROP_BOOL_TRUE;
187 set_mdast_scalar(arena, node_id, offset, &[value as u8]);
188 }
189 _ => return Ok(None),
190 },
191 PROP_INT => {
192 let parsed = value_str.parse::<i64>().ok().or_else(|| {
195 value_str
196 .parse::<f64>()
197 .ok()
198 .filter(|f| f.is_finite() && f.fract() == 0.0)
199 .map(|f| f as i64)
200 });
201 let node_type = arena.get_node(node_id).node_type;
202 let fitted = |max: u32| -> Result<u32, CommandError> {
203 match parsed {
204 Some(v) if (0..=max as i64).contains(&v) => Ok(v as u32),
205 _ => Err(CommandError::PropertyValueOutOfRange {
206 node_type: mdast_type_name(node_type),
207 name: prop_name.to_string(),
208 value: value_str.to_string(),
209 max,
210 }),
211 }
212 };
213 match slot {
214 S::U8 { offset } | S::CheckedTri { offset } => {
215 let value = fitted(u8::MAX as u32)?;
216 set_mdast_scalar(arena, node_id, offset, &[value as u8]);
217 }
218 S::U32 { offset } => {
219 let value = fitted(u32::MAX)?;
220 set_mdast_scalar(arena, node_id, offset, &value.to_le_bytes());
221 }
222 _ => return Ok(None),
223 }
224 }
225 PROP_NULL => match slot {
226 S::CheckedTri { offset } => set_mdast_scalar(arena, node_id, offset, &[2]),
228 S::Str { offset } => set_mdast_string_ref(arena, node_id, offset, StringRef::empty())?,
229 _ => return Ok(None),
230 },
231 _ => return Err(CommandError::InvalidPropertyValueType(value_type)),
232 }
233 Ok(Some(()))
234}
235
236fn set_mdast_string_ref(
238 arena: &mut Arena<Mdast>,
239 node_id: u32,
240 offset: usize,
241 sref: StringRef,
242) -> Result<(), CommandError> {
243 let node = arena.get_node(node_id);
244 let data_offset = node.data_offset as usize;
245 let data_len = node.data_len as usize;
246 if data_len < offset + 8 {
247 return Err(CommandError::TypeDataTooShort);
248 }
249 let abs_offset = data_offset + offset;
250 arena.type_data[abs_offset..abs_offset + 8].copy_from_slice(&sref.as_bytes());
251 Ok(())
252}
253
254fn set_mdast_scalar(arena: &mut Arena<Mdast>, node_id: u32, offset: usize, bytes: &[u8]) {
257 let node = arena.get_node(node_id);
258 let data_offset = node.data_offset as usize;
259 let data_len = node.data_len as usize;
260 if data_len >= offset + bytes.len() {
261 let abs_offset = data_offset + offset;
262 arena.type_data[abs_offset..abs_offset + bytes.len()].copy_from_slice(bytes);
263 }
264}
265
266fn escape_braces_in_html_text(html: &str) -> String {
274 let mut result = String::with_capacity(html.len());
275 let mut in_tag = false;
276 let mut in_quote: Option<char> = None;
277
278 let mut chars = html.chars().peekable();
279 while let Some(ch) = chars.next() {
280 if in_tag {
281 match ch {
282 '"' | '\'' if in_quote == Some(ch) => {
283 in_quote = None;
284 result.push(ch);
285 }
286 '"' | '\'' if in_quote.is_none() => {
287 in_quote = Some(ch);
288 result.push(ch);
289 }
290 '>' if in_quote.is_none() => {
291 in_tag = false;
292 result.push(ch);
293 }
294 _ => result.push(ch),
295 }
296 } else {
297 match ch {
298 '<' if chars.peek().copied().is_some_and(can_open_tag) => {
299 in_tag = true;
300 result.push(ch);
301 }
302 '{' => result.push_str("{'{'}"),
303 '}' => result.push_str("{'}'}"),
304 _ => result.push(ch),
305 }
306 }
307 }
308 result
309}
310
311fn can_open_tag(after: char) -> bool {
313 after.is_ascii_alphabetic() || matches!(after, '/' | '>' | '!' | '?' | '_' | '$')
314}
315
316#[derive(Debug, Clone, Copy)]
318pub struct MdastCommandOptions {
319 pub escape_raw_html_braces: bool,
325}
326
327impl Default for MdastCommandOptions {
328 fn default() -> Self {
329 Self {
330 escape_raw_html_braces: true,
331 }
332 }
333}
334
335fn emit_ref_node<K: ArenaKind>(ref_id: u32, builder: &mut ArenaBuilder<K>) -> u32 {
339 let id = builder.open_node_raw(REF_NODE_TYPE);
340 builder.set_data_current(&ref_id.to_le_bytes());
341 builder.close_node();
342 id
343}
344
345use crate::generated::encode::{
348 MAX_FIXED_TYPE_DATA, encode_hast_tail_from_ops, encode_mdast_tail_from_ops,
349 encode_mdast_type_data_from_ops,
350};
351
352pub(crate) fn alloc_opt_str<K: ArenaKind>(
353 builder: &mut ArenaBuilder<K>,
354 s: Option<&str>,
355) -> StringRef {
356 match s {
357 Some(v) if !v.is_empty() => builder.alloc_string(v),
358 _ => StringRef::empty(),
359 }
360}
361
362fn apply_hast_set_property(
368 arena: &mut Arena<Hast>,
369 node_id: u32,
370 prop_name: &str,
371 value_type: u8,
372 value_str: &str,
373) -> Result<(), CommandError> {
374 if node_id as usize >= arena.len() {
375 return Err(CommandError::InvalidNodeId(node_id));
376 }
377 if prop_name == "data" {
378 apply_data_property(arena, node_id, value_type, value_str);
379 return Ok(());
380 }
381
382 let raw_type = arena.get_node(node_id).node_type;
383 let node_type = HastNodeType::from_u8(raw_type)
384 .ok_or_else(|| CommandError::UnknownNodeType(format!("hast type 0x{raw_type:02x}")))?;
385
386 match node_type {
387 HastNodeType::Element => {
388 apply_hast_element_property(arena, node_id, prop_name, value_type, value_str)
389 }
390
391 HastNodeType::Text
392 | HastNodeType::Comment
393 | HastNodeType::Raw
394 | HastNodeType::MdxFlowExpression
395 | HastNodeType::MdxTextExpression
396 | HastNodeType::MdxEsm
397 if prop_name == "value" =>
398 {
399 let sref = arena.alloc_string(value_str);
400 let data = arena.get_type_data(node_id);
401 if data.len() >= 8 {
402 let data_offset = arena.get_node(node_id).data_offset as usize;
403 arena.type_data[data_offset..data_offset + 8].copy_from_slice(&sref.as_bytes());
404 Ok(())
405 } else {
406 Err(CommandError::TypeDataTooShort)
407 }
408 }
409
410 #[cfg(feature = "mdx")]
411 HastNodeType::MdxJsxElement | HastNodeType::MdxJsxTextElement => {
412 apply_hast_mdx_jsx_attribute(arena, node_id, prop_name, value_type, value_str)
413 }
414
415 _ => Err(CommandError::UnknownField {
416 node_type: node_type.name().to_string(),
417 name: prop_name.to_string(),
418 }),
419 }
420}
421
422#[cfg(feature = "mdx")]
436fn apply_hast_mdx_jsx_attribute(
437 arena: &mut Arena<Hast>,
438 node_id: u32,
439 attr_name: &str,
440 value_type: u8,
441 value_str: &str,
442) -> Result<(), CommandError> {
443 let old_data = arena.get_type_data(node_id).to_vec();
444 if old_data.len() < 16 {
445 return Err(CommandError::TypeDataTooShort);
446 }
447 let elem_name = decode_mdx_jsx_element_name(&old_data);
448 let explicit = decode_mdx_jsx_explicit(&old_data);
449 let attr_count = decode_mdx_jsx_attr_count(&old_data);
450
451 let (kind, val_ref) = match value_type {
453 PROP_BOOL_TRUE | PROP_NULL => (MDX_ATTR_BOOLEAN_PROP, StringRef::empty()),
454 PROP_BOOL_FALSE => (MDX_ATTR_LITERAL_PROP, arena.alloc_string("false")),
455 _ if value_str.is_empty() => (MDX_ATTR_LITERAL_PROP, StringRef::empty()),
456 _ => (MDX_ATTR_LITERAL_PROP, arena.alloc_string(value_str)),
457 };
458
459 let mut attrs: Vec<(u8, StringRef, StringRef)> = Vec::with_capacity(attr_count as usize + 1);
460 let mut name_ref: Option<StringRef> = None;
461 for i in 0..attr_count {
462 let (existing_kind, existing_name, existing_value) = decode_mdx_jsx_attr(&old_data, i);
463 if existing_kind != MDX_ATTR_SPREAD && arena.get_str(existing_name) == attr_name {
464 name_ref = Some(existing_name);
465 continue;
466 }
467 attrs.push((existing_kind, existing_name, existing_value));
468 }
469 let name_ref = name_ref.unwrap_or_else(|| arena.alloc_string(attr_name));
470 attrs.push((kind, name_ref, val_ref));
471
472 arena.set_type_data(
473 node_id,
474 &encode_mdx_jsx_element_data(elem_name, &attrs, explicit),
475 );
476 Ok(())
477}
478
479fn apply_hast_element_property(
485 arena: &mut Arena<Hast>,
486 node_id: u32,
487 prop_name: &str,
488 value_type: u8,
489 value_str: &str,
490) -> Result<(), CommandError> {
491 let node = arena.get_node(node_id);
493 let data_offset = node.data_offset as usize;
494 let data_len = node.data_len as usize;
495 if data_len < 16 {
496 return Err(CommandError::TypeDataTooShort);
497 }
498 let header = data_offset;
499 let old_prop_count =
500 u32::from_le_bytes(arena.type_data[header + 8..header + 12].try_into().unwrap()) as usize;
501
502 let mut found_index: Option<usize> = None;
505 for i in 0..old_prop_count {
506 let base = header + 16 + i * 20;
507 let name_off = u32::from_le_bytes(arena.type_data[base..base + 4].try_into().unwrap());
508 let name_len = u32::from_le_bytes(arena.type_data[base + 4..base + 8].try_into().unwrap());
509 let existing_name = arena.get_str(StringRef::new(name_off, name_len));
510 if existing_name == prop_name {
511 found_index = Some(i);
512 break;
513 }
514 }
515
516 let val_ref = if value_str.is_empty() {
520 StringRef::empty()
521 } else {
522 arena.alloc_string(value_str)
523 };
524
525 if let Some(idx) = found_index {
527 let base = header + 16 + idx * 20;
530 arena.type_data[base + 8] = value_type;
531 arena.type_data[base + 9..base + 12].copy_from_slice(&[0u8; 3]);
532 arena.type_data[base + 12..base + 16].copy_from_slice(&val_ref.offset.to_le_bytes());
533 arena.type_data[base + 16..base + 20].copy_from_slice(&val_ref.len.to_le_bytes());
534 } else {
535 let name_ref = arena.alloc_string(prop_name);
536 let new_offset = arena.type_data.len() as u32;
537 let new_prop_count = (old_prop_count + 1) as u32;
538
539 arena.type_data.extend_from_within(header..header + 8);
541 arena
542 .type_data
543 .extend_from_slice(&new_prop_count.to_le_bytes());
544 arena.type_data.extend_from_slice(&0u32.to_le_bytes());
545 if old_prop_count > 0 {
546 let props_start = header + 16;
547 let props_end = props_start + old_prop_count * 20;
548 arena.type_data.extend_from_within(props_start..props_end);
549 }
550 arena
551 .type_data
552 .extend_from_slice(&name_ref.offset.to_le_bytes());
553 arena
554 .type_data
555 .extend_from_slice(&name_ref.len.to_le_bytes());
556 arena.type_data.push(value_type);
557 arena.type_data.extend_from_slice(&[0u8; 3]);
558 arena
559 .type_data
560 .extend_from_slice(&val_ref.offset.to_le_bytes());
561 arena
562 .type_data
563 .extend_from_slice(&val_ref.len.to_le_bytes());
564
565 let new_len = (16 + new_prop_count as usize * 20) as u32;
566 let node = arena.get_node_mut(node_id);
567 node.data_offset = new_offset;
568 node.data_len = new_len;
569 }
570
571 Ok(())
572}
573
574trait OpCollector<'a>: Sized {
585 type Kind: ArenaKind;
586 const NUMERIC_OPS: bool;
590
591 fn open(node_type: u8) -> Self;
592 fn check_tag(tag: u8) -> Result<(), CommandError>;
593 fn finalize(&mut self, builder: &mut ArenaBuilder<Self::Kind>);
594 fn str_field(&mut self, field: u8, value: &'a str);
595 fn bool_field(&mut self, field: u8, value: bool);
596 fn prop(&mut self, name: &'a str, kind: u8, value: &'a str);
597 fn data(&mut self, bytes: &'a [u8]);
598 fn u8_field(&mut self, _field: u8, _value: u8) {}
599 fn u32_field(&mut self, _field: u8, _value: u32) {}
600 fn align(&mut self, _bytes: &'a [u8]) {}
601}
602
603const MAX_OPSTREAM_DEPTH: usize = 128;
607
608fn replay_opstream<'a, C: OpCollector<'a>>(
611 ops: &'a [u8],
612 builder: &mut ArenaBuilder<C::Kind>,
613 original_len: u32,
614 anchor: u32,
615) -> Result<Vec<u32>, CommandError> {
616 let mut reader = BufReader::new(ops);
617 let mut stack: Vec<C> = Vec::new();
618 let mut roots: Vec<u32> = Vec::new();
619
620 while reader.remaining() > 0 {
621 match reader.read_u8()? {
622 OP_OPEN => {
623 if let Some(c) = stack.last_mut() {
624 c.finalize(builder);
625 }
626 let node_type = reader.read_u8()?;
627 C::check_tag(node_type)?;
628 if !stack.is_empty() && node_type == <C::Kind as ArenaKind>::ROOT_TAG {
632 return Err(CommandError::UnencodableNodeType("root"));
633 }
634 if stack.len() >= MAX_OPSTREAM_DEPTH {
635 return Err(CommandError::OpstreamTooDeep(MAX_OPSTREAM_DEPTH));
636 }
637 let id = builder.open_node(node_type);
638 if stack.is_empty() {
639 roots.push(id);
640 }
641 stack.push(C::open(node_type));
642 }
643 OP_CLOSE => {
644 let Some(mut c) = stack.pop() else {
645 return Err(CommandError::UnbalancedOpstream);
646 };
647 c.finalize(builder);
648 builder.close_node();
649 }
650 OP_REF => {
651 if let Some(c) = stack.last_mut() {
652 c.finalize(builder);
653 }
654 let id = reader.read_u32()?;
655 if id >= original_len {
658 return Err(CommandError::InvalidNodeId(id));
659 }
660 let ref_id = emit_ref_node(id, builder);
661 if stack.is_empty() {
662 roots.push(ref_id);
663 }
664 }
665 OP_KEEP_CHILDREN => {
666 if let Some(c) = stack.last_mut() {
667 c.finalize(builder);
668 }
669 if anchor >= original_len {
670 return Err(CommandError::InvalidNodeId(anchor));
671 }
672 let children = builder.arena_ref().get_children(anchor).to_vec();
673 for child in children {
674 let ref_id = emit_ref_node(child, builder);
675 if stack.is_empty() {
676 roots.push(ref_id);
677 }
678 }
679 }
680 OP_STR => {
681 let field = reader.read_u8()?;
682 let len = reader.read_u32()? as usize;
683 let value = reader.read_str(len)?;
684 if let Some(c) = stack.last_mut() {
685 c.str_field(field, value);
686 }
687 }
688 OP_U8 if C::NUMERIC_OPS => {
689 let field = reader.read_u8()?;
690 let value = reader.read_u8()?;
691 if let Some(c) = stack.last_mut() {
692 c.u8_field(field, value);
693 }
694 }
695 OP_U32 if C::NUMERIC_OPS => {
696 let field = reader.read_u8()?;
697 let value = reader.read_u32()?;
698 if let Some(c) = stack.last_mut() {
699 c.u32_field(field, value);
700 }
701 }
702 OP_BOOL => {
703 let field = reader.read_u8()?;
704 let value = reader.read_u8()? != 0;
705 if let Some(c) = stack.last_mut() {
706 c.bool_field(field, value);
707 }
708 }
709 OP_PROP => {
710 let name_len = reader.read_u32()? as usize;
711 let name = reader.read_str(name_len)?;
712 let kind = reader.read_u8()?;
713 let val_len = reader.read_u32()? as usize;
714 let value = reader.read_str(val_len)?;
715 if let Some(c) = stack.last_mut() {
716 c.prop(name, kind, value);
717 }
718 }
719 OP_ALIGN if C::NUMERIC_OPS => {
720 let len = reader.read_u32()? as usize;
721 let bytes = reader.read_bytes(len)?;
722 if let Some(c) = stack.last_mut() {
723 c.align(bytes);
724 }
725 }
726 OP_DATA => {
727 let len = reader.read_u32()? as usize;
728 let bytes = reader.read_bytes(len)?;
729 if let Some(c) = stack.last_mut() {
730 c.data(bytes);
731 }
732 }
733 other => return Err(CommandError::UnknownCommand(other)),
734 }
735 }
736
737 if !stack.is_empty() {
738 return Err(CommandError::UnbalancedOpstream);
739 }
740 Ok(roots)
741}
742
743#[cfg(feature = "mdx")]
746pub(crate) fn intern_mdx_jsx_attrs<K: ArenaKind>(
747 props: &[(&str, u8, &str)],
748 builder: &mut ArenaBuilder<K>,
749) -> Vec<(u8, StringRef, StringRef)> {
750 let mut attrs = Vec::with_capacity(props.len());
751 for &(name, kind, value) in props {
752 let nr = if kind == MDX_ATTR_SPREAD {
753 StringRef::empty()
754 } else {
755 builder.alloc_string(name)
756 };
757 let vr = if kind == MDX_ATTR_BOOLEAN_PROP {
758 StringRef::empty()
759 } else {
760 builder.alloc_string(value)
761 };
762 attrs.push((kind, nr, vr));
763 }
764 attrs
765}
766
767#[derive(Default)]
770pub(crate) struct FieldCollector<'a> {
771 node_type: u8,
772 finalized: bool,
773 pub(crate) strs: [Option<&'a str>; OF_FIELD_COUNT],
774 pub(crate) depth: Option<u8>,
775 checked: Option<u8>,
776 start: Option<u32>,
777 ordered: Option<bool>,
778 spread: Option<bool>,
779 pub(crate) props: Vec<(&'a str, u8, &'a str)>,
781 pub(crate) align: Option<&'a [u8]>,
783 pub(crate) explicit: Option<bool>,
785 data: Option<&'a [u8]>,
786}
787
788fn finalize_collector(c: &mut FieldCollector<'_>, builder: &mut ArenaBuilder<Mdast>) {
790 const LIST: u8 = MdastNodeType::List as u8;
791 const LIST_ITEM: u8 = MdastNodeType::ListItem as u8;
792 const DESCRIPTION_DETAILS: u8 = MdastNodeType::DescriptionDetails as u8;
793
794 if c.finalized {
795 return;
796 }
797 c.finalized = true;
798 let mut fixed = [0u8; MAX_FIXED_TYPE_DATA];
799 if let Some(len) = encode_mdast_type_data_from_ops(c, c.node_type, builder, &mut fixed) {
800 builder.set_data_current(&fixed[..len]);
801 } else if let Some(type_data) = encode_mdast_tail_from_ops(c, c.node_type, builder) {
802 builder.set_data_current(&type_data);
804 } else {
805 let type_data: Vec<u8> = match c.node_type {
806 LIST => encode_list_data(
807 c.ordered.unwrap_or(false),
808 c.start.unwrap_or(1),
809 c.spread.unwrap_or(false),
810 ),
811 LIST_ITEM => encode_list_item_data(c.checked.unwrap_or(2), c.spread.unwrap_or(false)),
813 DESCRIPTION_DETAILS => encode_description_details_data(c.spread.unwrap_or(false)),
814 _ => Vec::new(),
816 };
817 if !type_data.is_empty() {
818 builder.set_data_current(&type_data);
819 }
820 }
821 if let Some(data) = c.data {
822 let id = builder.current_node_id();
823 builder.arena_mut().set_node_data(id, data.to_vec());
824 }
825}
826
827impl<'a> OpCollector<'a> for FieldCollector<'a> {
828 type Kind = Mdast;
829 const NUMERIC_OPS: bool = true;
830
831 fn open(node_type: u8) -> Self {
832 FieldCollector {
833 node_type,
834 ..Default::default()
835 }
836 }
837
838 fn check_tag(tag: u8) -> Result<(), CommandError> {
841 let known = MdastNodeType::from_u8(tag).is_some();
842 #[cfg(not(feature = "mdx"))]
843 let known = known
844 && !matches!(
845 MdastNodeType::from_u8(tag),
846 Some(
847 MdastNodeType::MdxJsxFlowElement
848 | MdastNodeType::MdxJsxTextElement
849 | MdastNodeType::MdxFlowExpression
850 | MdastNodeType::MdxTextExpression
851 | MdastNodeType::MdxjsEsm
852 )
853 );
854 if known {
855 Ok(())
856 } else {
857 Err(CommandError::UnknownNodeType(format!(
858 "op-stream tag {tag}"
859 )))
860 }
861 }
862
863 fn finalize(&mut self, builder: &mut ArenaBuilder<Mdast>) {
864 finalize_collector(self, builder);
865 }
866
867 fn str_field(&mut self, field: u8, value: &'a str) {
868 let field = field as usize;
869 if field < self.strs.len() {
870 self.strs[field] = Some(value);
871 }
872 }
873
874 fn bool_field(&mut self, field: u8, value: bool) {
875 match field {
876 OF_ORDERED => self.ordered = Some(value),
877 OF_SPREAD => self.spread = Some(value),
878 OF_EXPLICIT => self.explicit = Some(value),
879 _ => {}
880 }
881 }
882
883 fn prop(&mut self, name: &'a str, kind: u8, value: &'a str) {
884 self.props.push((name, kind, value));
885 }
886
887 fn data(&mut self, bytes: &'a [u8]) {
888 self.data = Some(bytes);
889 }
890
891 fn u8_field(&mut self, field: u8, value: u8) {
892 match field {
893 OF_DEPTH => self.depth = Some(value),
894 OF_CHECKED => self.checked = Some(value),
895 _ => {}
896 }
897 }
898
899 fn u32_field(&mut self, field: u8, value: u32) {
900 if field == OF_START {
901 self.start = Some(value);
902 }
903 }
904
905 fn align(&mut self, bytes: &'a [u8]) {
906 self.align = Some(bytes);
907 }
908}
909
910fn replay_mdast_opstream(
911 ops: &[u8],
912 builder: &mut ArenaBuilder<Mdast>,
913 original_len: u32,
914 anchor: u32,
915) -> Result<Vec<u32>, CommandError> {
916 replay_opstream::<FieldCollector>(ops, builder, original_len, anchor)
917}
918
919fn read_mdast_payload(
923 reader: &mut BufReader<'_>,
924 parse_markdown: &dyn Fn(&str) -> Arena<Mdast>,
925 builder: &mut ArenaBuilder<Mdast>,
926 original_len: u32,
927 anchor: u32,
928 options: MdastCommandOptions,
929) -> Result<(PatchContent<Mdast>, bool), CommandError> {
930 let payload_type = reader.read_u8()?;
931
932 match payload_type {
933 PAYLOAD_RAW => {
934 let flags = reader.read_u8()?;
935 let len = reader.read_u32()? as usize;
936 let raw = reader.read_str(len)?;
937 let tree = if flags & RAW_LITERAL_BRACES != 0 && options.escape_raw_html_braces {
941 parse_markdown(&escape_braces_in_html_text(raw))
942 } else {
943 parse_markdown(raw)
944 };
945 Ok((PatchContent::Tree(tree), false))
946 }
947 PAYLOAD_OPSTREAM => {
948 let len = reader.read_u32()? as usize;
949 let ops = reader.read_bytes(len)?;
950 Ok((
951 PatchContent::Grafted(replay_mdast_opstream(ops, builder, original_len, anchor)?),
952 false,
953 ))
954 }
955 other => Err(CommandError::UnknownPayloadType(other)),
956 }
957}
958
959#[derive(Default)]
963pub(crate) struct HastFieldCollector<'a> {
964 node_type: u8,
965 finalized: bool,
966 pub(crate) tag: Option<&'a str>,
968 value: Option<&'a str>,
969 pub(crate) props: Vec<(&'a str, u8, &'a str)>,
970 pub(crate) explicit: Option<bool>,
972 data: Option<&'a [u8]>,
973}
974
975fn finalize_hast_collector(c: &mut HastFieldCollector<'_>, builder: &mut ArenaBuilder<Hast>) {
976 const TEXT: u8 = HastNodeType::Text as u8;
977 const COMMENT: u8 = HastNodeType::Comment as u8;
978 const RAW: u8 = HastNodeType::Raw as u8;
979 const MDX_FLOW_EXPRESSION: u8 = HastNodeType::MdxFlowExpression as u8;
980 const MDX_ESM: u8 = HastNodeType::MdxEsm as u8;
981 const MDX_TEXT_EXPRESSION: u8 = HastNodeType::MdxTextExpression as u8;
982
983 if c.finalized {
984 return;
985 }
986 c.finalized = true;
987 if let Some(type_data) = encode_hast_tail_from_ops(c, c.node_type, builder) {
988 builder.set_data_current(&type_data);
990 } else {
991 let type_data: Vec<u8> = match c.node_type {
992 TEXT | COMMENT | RAW | MDX_FLOW_EXPRESSION | MDX_ESM | MDX_TEXT_EXPRESSION => {
993 let sref = builder.alloc_string(c.value.unwrap_or(""));
994 encode_string_ref_data(sref)
995 }
996 _ => Vec::new(),
998 };
999 if !type_data.is_empty() {
1000 builder.set_data_current(&type_data);
1001 }
1002 }
1003 if let Some(data) = c.data {
1004 let id = builder.current_node_id();
1005 builder.arena_mut().set_node_data(id, data.to_vec());
1006 }
1007}
1008
1009impl<'a> OpCollector<'a> for HastFieldCollector<'a> {
1010 type Kind = Hast;
1011 const NUMERIC_OPS: bool = false;
1012
1013 fn open(node_type: u8) -> Self {
1014 HastFieldCollector {
1015 node_type,
1016 ..Default::default()
1017 }
1018 }
1019
1020 fn check_tag(tag: u8) -> Result<(), CommandError> {
1022 if HastNodeType::from_u8(tag) == Some(HastNodeType::Doctype) {
1026 return Err(CommandError::UnencodableNodeType("doctype"));
1027 }
1028 let known = HastNodeType::from_u8(tag).is_some();
1029 #[cfg(not(feature = "mdx"))]
1030 let known = known
1031 && !matches!(
1032 HastNodeType::from_u8(tag),
1033 Some(
1034 HastNodeType::MdxJsxElement
1035 | HastNodeType::MdxJsxTextElement
1036 | HastNodeType::MdxFlowExpression
1037 | HastNodeType::MdxEsm
1038 | HastNodeType::MdxTextExpression
1039 )
1040 );
1041 if known {
1042 Ok(())
1043 } else {
1044 Err(CommandError::UnknownNodeType(format!(
1045 "op-stream tag {tag}"
1046 )))
1047 }
1048 }
1049
1050 fn finalize(&mut self, builder: &mut ArenaBuilder<Hast>) {
1051 finalize_hast_collector(self, builder);
1052 }
1053
1054 fn str_field(&mut self, field: u8, value: &'a str) {
1055 match field {
1056 OF_TAGNAME | OF_NAME => self.tag = Some(value),
1057 OF_VALUE => self.value = Some(value),
1058 _ => {}
1059 }
1060 }
1061
1062 fn bool_field(&mut self, field: u8, value: bool) {
1063 if field == OF_EXPLICIT {
1064 self.explicit = Some(value);
1065 }
1066 }
1067
1068 fn prop(&mut self, name: &'a str, kind: u8, value: &'a str) {
1069 self.props.push((name, kind, value));
1070 }
1071
1072 fn data(&mut self, bytes: &'a [u8]) {
1073 self.data = Some(bytes);
1074 }
1075}
1076
1077fn replay_hast_opstream(
1078 ops: &[u8],
1079 builder: &mut ArenaBuilder<Hast>,
1080 original_len: u32,
1081 anchor: u32,
1082) -> Result<Vec<u32>, CommandError> {
1083 replay_opstream::<HastFieldCollector>(ops, builder, original_len, anchor)
1084}
1085
1086fn read_hast_payload(
1090 reader: &mut BufReader<'_>,
1091 builder: &mut ArenaBuilder<Hast>,
1092 original_len: u32,
1093 anchor: u32,
1094 for_wrap: bool,
1095) -> Result<(PatchContent<Hast>, bool), CommandError> {
1096 let payload_type = reader.read_u8()?;
1097
1098 match payload_type {
1099 PAYLOAD_OPSTREAM => {
1100 let len = reader.read_u32()? as usize;
1101 let ops = reader.read_bytes(len)?;
1102 Ok((
1103 PatchContent::Grafted(replay_hast_opstream(ops, builder, original_len, anchor)?),
1104 false,
1105 ))
1106 }
1107 PAYLOAD_RAW if for_wrap => {
1108 let _flags = reader.read_u8()?;
1111 let len = reader.read_u32()? as usize;
1112 let raw = reader.read_str(len)?;
1113 Ok((PatchContent::Tree(hast_wrap_arena_from_html(raw)?), false))
1114 }
1115 other => Err(CommandError::UnknownPayloadType(other)),
1116 }
1117}
1118
1119#[cfg(feature = "from-html")]
1120fn hast_wrap_arena_from_html(raw: &str) -> Result<Arena<Hast>, CommandError> {
1121 satteri_ast::hast::html_fragment_to_wrap_arena(raw).map_err(CommandError::InvalidRawWrapper)
1122}
1123
1124#[cfg(not(feature = "from-html"))]
1126fn hast_wrap_arena_from_html(_raw: &str) -> Result<Arena<Hast>, CommandError> {
1127 Err(CommandError::InvalidRawWrapper(
1128 "requires HTML parsing, which this build omits (from-html feature)".to_string(),
1129 ))
1130}
1131
1132fn is_mdast_leaf(node_type: u8) -> bool {
1135 use MdastNodeType::*;
1136 matches!(
1137 MdastNodeType::from_u8(node_type),
1138 Some(
1139 ThematicBreak
1140 | Html
1141 | Code
1142 | Definition
1143 | Text
1144 | InlineCode
1145 | Break
1146 | Image
1147 | ImageReference
1148 | FootnoteReference
1149 | Yaml
1150 | Toml
1151 | Math
1152 | InlineMath
1153 | MdxFlowExpression
1154 | MdxTextExpression
1155 | MdxjsEsm
1156 )
1157 )
1158}
1159
1160fn mdast_wrap_arena_from_tree(mut tree: Arena<Mdast>) -> Result<Arena<Mdast>, CommandError> {
1164 let roots: &[u32] = if tree.is_empty() {
1165 &[]
1166 } else {
1167 tree.get_children(0)
1168 };
1169 let &[wrapper] = roots else {
1170 return Err(CommandError::InvalidRawWrapper(
1171 "must parse to exactly one block".to_string(),
1172 ));
1173 };
1174 let node = *tree.get_node(wrapper);
1175 if is_mdast_leaf(node.node_type) {
1176 let name = MdastNodeType::from_u8(node.node_type).map_or("node", MdastNodeType::name);
1177 return Err(CommandError::InvalidRawWrapper(format!(
1178 "parses to a {name}, which cannot hold the wrapped node"
1179 )));
1180 }
1181 let children = tree.get_children(wrapper).to_vec();
1182 let type_data = tree.get_type_data(wrapper).to_vec();
1183 let node_data = tree.get_node_data(wrapper).map(<[u8]>::to_vec);
1184 tree.get_node_mut(0).node_type = node.node_type;
1185 tree.set_position(
1186 0,
1187 node.start_offset,
1188 node.end_offset,
1189 node.start_line,
1190 node.start_column,
1191 node.end_line,
1192 node.end_column,
1193 );
1194 tree.set_type_data(0, &type_data);
1195 tree.set_children(0, &children);
1196 if let Some(data) = node_data {
1197 tree.set_node_data(0, data);
1198 }
1199 Ok(tree)
1200}
1201
1202fn reject_void_wrap_parent(
1205 parent_tree: &PatchContent<Hast>,
1206 grafted: &Arena<Hast>,
1207) -> Result<(), CommandError> {
1208 let (source, wrapper) = match parent_tree {
1209 PatchContent::Tree(tree) if !tree.is_empty() => (tree, 0),
1210 PatchContent::Grafted(roots) => match roots.first() {
1211 Some(&root) => (grafted, root),
1212 None => return Ok(()),
1213 },
1214 PatchContent::Tree(_) => return Ok(()),
1215 };
1216 if source.get_node(wrapper).node_type != HastNodeType::Element as u8 {
1217 return Ok(());
1218 }
1219 let tag = source.get_str(decode_element_tag(source.get_type_data(wrapper)));
1220 if is_void_element(tag) {
1221 return Err(CommandError::VoidWrapParent(tag.to_string()));
1222 }
1223 Ok(())
1224}
1225
1226pub fn apply_mdast_commands(
1249 arena: Arena<Mdast>,
1250 command_buf: &[u8],
1251 parse_markdown: &dyn Fn(&str) -> Arena<Mdast>,
1252) -> Result<Arena<Mdast>, CommandError> {
1253 apply_mdast_commands_with_options(
1254 arena,
1255 command_buf,
1256 parse_markdown,
1257 MdastCommandOptions::default(),
1258 )
1259}
1260
1261pub fn apply_mdast_commands_with_options(
1263 arena: Arena<Mdast>,
1264 command_buf: &[u8],
1265 parse_markdown: &dyn Fn(&str) -> Arena<Mdast>,
1266 options: MdastCommandOptions,
1267) -> Result<Arena<Mdast>, CommandError> {
1268 let (arena, dropped) =
1269 apply_mdast_commands_lenient_with_options(arena, command_buf, parse_markdown, options)?;
1270 if let Some(anchor) = dropped.first() {
1271 return Err(CommandError::PatchOnRemovedSubtree(*anchor));
1272 }
1273 Ok(arena)
1274}
1275
1276pub fn apply_mdast_commands_lenient(
1283 arena: Arena<Mdast>,
1284 command_buf: &[u8],
1285 parse_markdown: &dyn Fn(&str) -> Arena<Mdast>,
1286) -> Result<(Arena<Mdast>, Vec<u32>), CommandError> {
1287 apply_mdast_commands_lenient_with_options(
1288 arena,
1289 command_buf,
1290 parse_markdown,
1291 MdastCommandOptions::default(),
1292 )
1293}
1294
1295pub fn apply_mdast_commands_lenient_with_options(
1298 mut arena: Arena<Mdast>,
1299 command_buf: &[u8],
1300 parse_markdown: &dyn Fn(&str) -> Arena<Mdast>,
1301 options: MdastCommandOptions,
1302) -> Result<(Arena<Mdast>, Vec<u32>), CommandError> {
1303 if command_buf.is_empty() {
1304 return Ok((arena, Vec::new()));
1305 }
1306
1307 let original_len = arena.len() as u32;
1311 let mut builder =
1312 ArenaBuilder::from_arena(std::mem::replace(&mut arena, Arena::new(String::new())));
1313 let mut patches: Vec<Patch<Mdast>> = Vec::new();
1314 let mut reader = BufReader::new(command_buf);
1315
1316 while reader.remaining() > 0 {
1317 let cmd = reader.read_u8()?;
1318
1319 match cmd {
1320 CMD_REMOVE => {
1321 let node_id = reader.read_anchor(original_len)?;
1322 patches.push(Patch::Remove { node_id });
1323 }
1324
1325 CMD_SET_PROPERTY => {
1326 let node_id = reader.read_anchor(original_len)?;
1327 let value_type = reader.read_u8()?;
1328 let name_len = reader.read_u32()? as usize;
1329 let name = reader.read_str(name_len)?;
1330 let value_len = reader.read_u32()? as usize;
1331 let value = reader.read_str(value_len)?;
1332 apply_mdast_set_property(builder.arena_mut(), node_id, name, value_type, value)?;
1333 }
1334
1335 CMD_INSERT_BEFORE => {
1336 let node_id = reader.read_anchor(original_len)?;
1337 let (new_tree, _) = read_mdast_payload(
1338 &mut reader,
1339 parse_markdown,
1340 &mut builder,
1341 original_len,
1342 node_id,
1343 options,
1344 )?;
1345 patches.push(Patch::InsertBefore { node_id, new_tree });
1346 }
1347
1348 CMD_INSERT_AFTER => {
1349 let node_id = reader.read_anchor(original_len)?;
1350 let (new_tree, _) = read_mdast_payload(
1351 &mut reader,
1352 parse_markdown,
1353 &mut builder,
1354 original_len,
1355 node_id,
1356 options,
1357 )?;
1358 patches.push(Patch::InsertAfter { node_id, new_tree });
1359 }
1360
1361 CMD_PREPEND_CHILD => {
1362 let node_id = reader.read_anchor(original_len)?;
1363 let (child_tree, _) = read_mdast_payload(
1364 &mut reader,
1365 parse_markdown,
1366 &mut builder,
1367 original_len,
1368 node_id,
1369 options,
1370 )?;
1371 patches.push(Patch::PrependChild {
1372 node_id,
1373 child_tree,
1374 });
1375 }
1376
1377 CMD_APPEND_CHILD => {
1378 let node_id = reader.read_anchor(original_len)?;
1379 let (child_tree, _) = read_mdast_payload(
1380 &mut reader,
1381 parse_markdown,
1382 &mut builder,
1383 original_len,
1384 node_id,
1385 options,
1386 )?;
1387 patches.push(Patch::AppendChild {
1388 node_id,
1389 child_tree,
1390 });
1391 }
1392
1393 CMD_WRAP => {
1394 let node_id = reader.read_anchor(original_len)?;
1395 let (parent_tree, _) = read_mdast_payload(
1396 &mut reader,
1397 parse_markdown,
1398 &mut builder,
1399 original_len,
1400 node_id,
1401 options,
1402 )?;
1403 let parent_tree = match parent_tree {
1404 PatchContent::Tree(tree) => {
1405 PatchContent::Tree(mdast_wrap_arena_from_tree(tree)?)
1406 }
1407 grafted => grafted,
1408 };
1409 patches.push(Patch::Wrap {
1410 node_id,
1411 parent_tree,
1412 });
1413 }
1414
1415 CMD_REPLACE => {
1416 let node_id = reader.read_anchor(original_len)?;
1417 let (new_tree, keep_children) = read_mdast_payload(
1418 &mut reader,
1419 parse_markdown,
1420 &mut builder,
1421 original_len,
1422 node_id,
1423 options,
1424 )?;
1425 patches.push(Patch::Replace {
1426 node_id,
1427 new_tree,
1428 keep_children,
1429 });
1430 }
1431
1432 CMD_SET_CHILDREN => {
1433 let node_id = reader.read_anchor(original_len)?;
1434 let (new_children, _) = read_mdast_payload(
1435 &mut reader,
1436 parse_markdown,
1437 &mut builder,
1438 original_len,
1439 node_id,
1440 options,
1441 )?;
1442 patches.push(Patch::SetChildren {
1443 node_id,
1444 new_children,
1445 });
1446 }
1447
1448 other => return Err(CommandError::UnknownCommand(other)),
1449 }
1450 }
1451
1452 let mut arena = builder.finish();
1453 if patches.is_empty() {
1454 Ok((arena, Vec::new()))
1455 } else {
1456 let dropped = satteri_ast::patch::apply_patches_in_place(&mut arena, &patches)?;
1457 Ok((arena, dropped))
1458 }
1459}
1460
1461pub fn apply_hast_commands(
1479 arena: Arena<Hast>,
1480 command_buf: &[u8],
1481) -> Result<Arena<Hast>, CommandError> {
1482 let (arena, dropped) = apply_hast_commands_lenient(arena, command_buf)?;
1483 if let Some(anchor) = dropped.first() {
1484 return Err(CommandError::PatchOnRemovedSubtree(*anchor));
1485 }
1486 Ok(arena)
1487}
1488
1489pub fn apply_hast_commands_lenient(
1495 mut arena: Arena<Hast>,
1496 command_buf: &[u8],
1497) -> Result<(Arena<Hast>, Vec<u32>), CommandError> {
1498 if command_buf.is_empty() {
1499 return Ok((arena, Vec::new()));
1500 }
1501
1502 let original_len = arena.len() as u32;
1505 let mut builder =
1506 ArenaBuilder::from_arena(std::mem::replace(&mut arena, Arena::new(String::new())));
1507 let mut patches: Vec<Patch<Hast>> = Vec::new();
1508 let mut reader = BufReader::new(command_buf);
1509
1510 while reader.remaining() > 0 {
1511 let cmd = reader.read_u8()?;
1512
1513 match cmd {
1514 CMD_REMOVE => {
1515 let node_id = reader.read_anchor(original_len)?;
1516 patches.push(Patch::Remove { node_id });
1517 }
1518
1519 CMD_SET_PROPERTY => {
1520 let node_id = reader.read_anchor(original_len)?;
1521 let value_type = reader.read_u8()?;
1522 let name_len = reader.read_u32()? as usize;
1523 let name = reader.read_str(name_len)?;
1524 let value_len = reader.read_u32()? as usize;
1525 let value = reader.read_str(value_len)?;
1526 apply_hast_set_property(builder.arena_mut(), node_id, name, value_type, value)?;
1527 }
1528
1529 CMD_INSERT_BEFORE => {
1530 let node_id = reader.read_anchor(original_len)?;
1531 let (new_tree, _) =
1532 read_hast_payload(&mut reader, &mut builder, original_len, node_id, false)?;
1533 patches.push(Patch::InsertBefore { node_id, new_tree });
1534 }
1535
1536 CMD_INSERT_AFTER => {
1537 let node_id = reader.read_anchor(original_len)?;
1538 let (new_tree, _) =
1539 read_hast_payload(&mut reader, &mut builder, original_len, node_id, false)?;
1540 patches.push(Patch::InsertAfter { node_id, new_tree });
1541 }
1542
1543 CMD_PREPEND_CHILD => {
1544 let node_id = reader.read_anchor(original_len)?;
1545 let (child_tree, _) =
1546 read_hast_payload(&mut reader, &mut builder, original_len, node_id, false)?;
1547 patches.push(Patch::PrependChild {
1548 node_id,
1549 child_tree,
1550 });
1551 }
1552
1553 CMD_APPEND_CHILD => {
1554 let node_id = reader.read_anchor(original_len)?;
1555 let (child_tree, _) =
1556 read_hast_payload(&mut reader, &mut builder, original_len, node_id, false)?;
1557 patches.push(Patch::AppendChild {
1558 node_id,
1559 child_tree,
1560 });
1561 }
1562
1563 CMD_WRAP => {
1564 let node_id = reader.read_anchor(original_len)?;
1565 let (parent_tree, _) =
1566 read_hast_payload(&mut reader, &mut builder, original_len, node_id, true)?;
1567 reject_void_wrap_parent(&parent_tree, builder.arena_mut())?;
1568 patches.push(Patch::Wrap {
1569 node_id,
1570 parent_tree,
1571 });
1572 }
1573
1574 CMD_REPLACE => {
1575 let node_id = reader.read_anchor(original_len)?;
1576 let (new_tree, keep_children) =
1577 read_hast_payload(&mut reader, &mut builder, original_len, node_id, false)?;
1578 patches.push(Patch::Replace {
1579 node_id,
1580 new_tree,
1581 keep_children,
1582 });
1583 }
1584
1585 CMD_SET_CHILDREN => {
1586 let node_id = reader.read_anchor(original_len)?;
1587 let (new_children, _) =
1588 read_hast_payload(&mut reader, &mut builder, original_len, node_id, false)?;
1589 patches.push(Patch::SetChildren {
1590 node_id,
1591 new_children,
1592 });
1593 }
1594
1595 other => return Err(CommandError::UnknownCommand(other)),
1596 }
1597 }
1598
1599 let mut arena = builder.finish();
1600 if patches.is_empty() {
1601 Ok((arena, Vec::new()))
1602 } else {
1603 let dropped = satteri_ast::patch::apply_patches_in_place(&mut arena, &patches)?;
1604 Ok((arena, dropped))
1605 }
1606}
1607
1608#[cfg(test)]
1609mod tests {
1610 use super::*;
1611
1612 fn replay_mdast_for_test(
1615 ops: &[u8],
1616 orig: &Arena<Mdast>,
1617 anchor: u32,
1618 ) -> Result<(Arena<Mdast>, Vec<u32>), CommandError> {
1619 let original_len = orig.len() as u32;
1620 let mut builder = ArenaBuilder::from_arena(orig.clone());
1621 let roots = replay_mdast_opstream(ops, &mut builder, original_len, anchor)?;
1622 Ok((builder.finish(), roots))
1623 }
1624
1625 fn replay_hast_for_test(
1626 ops: &[u8],
1627 orig: &Arena<Hast>,
1628 anchor: u32,
1629 ) -> Result<(Arena<Hast>, Vec<u32>), CommandError> {
1630 let original_len = orig.len() as u32;
1631 let mut builder = ArenaBuilder::from_arena(orig.clone());
1632 let roots = replay_hast_opstream(ops, &mut builder, original_len, anchor)?;
1633 Ok((builder.finish(), roots))
1634 }
1635
1636 use satteri_ast::shared::PROP_INT;
1637
1638 fn op_open(b: &mut Vec<u8>, t: MdastNodeType) {
1639 b.push(OP_OPEN);
1640 b.push(t as u8);
1641 }
1642 fn op_close(b: &mut Vec<u8>) {
1643 b.push(OP_CLOSE);
1644 }
1645 fn op_str(b: &mut Vec<u8>, field: u8, s: &str) {
1646 b.push(OP_STR);
1647 b.push(field);
1648 b.extend_from_slice(&(s.len() as u32).to_le_bytes());
1649 b.extend_from_slice(s.as_bytes());
1650 }
1651 fn op_u8(b: &mut Vec<u8>, field: u8, v: u8) {
1652 b.push(OP_U8);
1653 b.push(field);
1654 b.push(v);
1655 }
1656
1657 #[test]
1658 fn opstream_replay_builds_subtree() {
1659 let mut ops = Vec::new();
1661 op_open(&mut ops, MdastNodeType::Blockquote);
1662 op_open(&mut ops, MdastNodeType::Heading);
1663 op_u8(&mut ops, OF_DEPTH, 3);
1664 op_open(&mut ops, MdastNodeType::Text);
1665 op_str(&mut ops, OF_VALUE, "Note");
1666 op_close(&mut ops);
1667 op_close(&mut ops);
1668 op_open(&mut ops, MdastNodeType::Paragraph);
1669 op_open(&mut ops, MdastNodeType::Text);
1670 op_str(&mut ops, OF_VALUE, "Body");
1671 op_close(&mut ops);
1672 op_close(&mut ops);
1673 op_close(&mut ops);
1674
1675 let empty = ArenaBuilder::<Mdast>::new(String::new()).finish();
1676 let (arena, roots) = replay_mdast_for_test(&ops, &empty, 0).unwrap();
1677
1678 let bq = roots[0];
1679 assert_eq!(
1680 arena.get_node(bq).node_type,
1681 MdastNodeType::Blockquote as u8
1682 );
1683 let top = arena.get_children(bq).to_vec();
1684 assert_eq!(top.len(), 2);
1685 let h = top[0];
1687 assert_eq!(arena.get_node(h).node_type, MdastNodeType::Heading as u8);
1688 assert_eq!(decode_heading_data(arena.get_type_data(h)).depth, 3);
1689 let h_text = arena.get_children(h)[0];
1690 assert_eq!(arena.get_node(h_text).node_type, MdastNodeType::Text as u8);
1691 let sref = decode_string_ref_data(arena.get_type_data(h_text));
1692 assert_eq!(arena.get_str(sref), "Note");
1693 let p = top[1];
1695 assert_eq!(arena.get_node(p).node_type, MdastNodeType::Paragraph as u8);
1696 let p_text = arena.get_children(p)[0];
1697 assert_eq!(
1698 arena.get_str(decode_string_ref_data(arena.get_type_data(p_text))),
1699 "Body"
1700 );
1701 }
1702
1703 #[test]
1704 fn opstream_replay_rejects_unbalanced_close() {
1705 let empty = ArenaBuilder::<Mdast>::new(String::new()).finish();
1706 let err = replay_mdast_for_test(&[OP_CLOSE], &empty, 0).unwrap_err();
1707 assert!(matches!(err, CommandError::UnbalancedOpstream));
1708
1709 let empty_hast = ArenaBuilder::<Hast>::new(String::new()).finish();
1710 let err = replay_hast_for_test(&[OP_CLOSE], &empty_hast, 0).unwrap_err();
1711 assert!(matches!(err, CommandError::UnbalancedOpstream));
1712
1713 let mut ops = Vec::new();
1715 op_open(&mut ops, MdastNodeType::Paragraph);
1716 op_close(&mut ops);
1717 op_close(&mut ops);
1718 let err = replay_mdast_for_test(&ops, &empty, 0).unwrap_err();
1719 assert!(matches!(err, CommandError::UnbalancedOpstream));
1720 }
1721
1722 #[test]
1723 fn opstream_replay_rejects_unclosed_node() {
1724 let mut ops = Vec::new();
1727 op_open(&mut ops, MdastNodeType::Heading);
1728 op_u8(&mut ops, OF_DEPTH, 2);
1729
1730 let empty = ArenaBuilder::<Mdast>::new(String::new()).finish();
1731 let err = replay_mdast_for_test(&ops, &empty, 0).unwrap_err();
1732 assert!(matches!(err, CommandError::UnbalancedOpstream));
1733
1734 let hast_ops = vec![OP_OPEN, HastNodeType::Element as u8];
1735 let empty_hast = ArenaBuilder::<Hast>::new(String::new()).finish();
1736 let err = replay_hast_for_test(&hast_ops, &empty_hast, 0).unwrap_err();
1737 assert!(matches!(err, CommandError::UnbalancedOpstream));
1738 }
1739
1740 #[test]
1741 fn opstream_keep_children_rejects_out_of_range_anchor() {
1742 let orig = test_parse_markdown("Hello");
1743 let bad_anchor = orig.len() as u32;
1744
1745 let mut ops = Vec::new();
1746 op_open(&mut ops, MdastNodeType::Heading);
1747 ops.push(OP_KEEP_CHILDREN);
1748 op_close(&mut ops);
1749
1750 let err = replay_mdast_for_test(&ops, &orig, bad_anchor).unwrap_err();
1751 assert!(matches!(err, CommandError::InvalidNodeId(id) if id == bad_anchor));
1752 }
1753
1754 #[test]
1755 fn set_property_rejects_out_of_range_node_id() {
1756 let arena = build_hello_world();
1757 let bad_id = arena.len() as u32;
1758 let mut buf = Vec::new();
1759 push_set_property(&mut buf, bad_id, PROP_INT, "depth", "3");
1760 let err = apply_mdast_commands(arena, &buf, &test_parse_markdown).unwrap_err();
1761 assert!(matches!(err, CommandError::InvalidNodeId(id) if id == bad_id));
1762 assert!(err.to_string().contains("invalid node id"));
1763
1764 let hast = build_hast_element(&[]);
1765 let bad_id = hast.len() as u32;
1766 let mut buf = Vec::new();
1767 push_set_property(&mut buf, bad_id, PROP_STRING, "class", "x");
1768 let err = apply_hast_commands(hast, &buf).unwrap_err();
1769 assert!(matches!(err, CommandError::InvalidNodeId(id) if id == bad_id));
1770 }
1771
1772 #[test]
1773 fn opstream_replay_rejects_unknown_tags() {
1774 let empty = ArenaBuilder::<Mdast>::new(String::new()).finish();
1775 let err = replay_mdast_for_test(&[OP_OPEN, 200], &empty, 0).unwrap_err();
1776 assert!(matches!(err, CommandError::UnknownNodeType(_)));
1777
1778 let empty_hast = ArenaBuilder::<Hast>::new(String::new()).finish();
1779 let err = replay_hast_for_test(&[OP_OPEN, 200], &empty_hast, 0).unwrap_err();
1780 assert!(matches!(err, CommandError::UnknownNodeType(_)));
1781 }
1782
1783 #[test]
1784 fn opstream_keep_children_splices_original_children() {
1785 let orig = test_parse_markdown("Hello");
1788 let para = orig.get_children(0)[0];
1789 let orig_text = orig.get_children(para)[0];
1790
1791 let mut ops = Vec::new();
1792 op_open(&mut ops, MdastNodeType::Heading);
1793 op_u8(&mut ops, OF_DEPTH, 2);
1794 ops.push(OP_KEEP_CHILDREN);
1795 op_close(&mut ops);
1796
1797 let (arena, roots) = replay_mdast_for_test(&ops, &orig, para).unwrap();
1798 let heading = roots[0];
1799 assert_eq!(
1800 arena.get_node(heading).node_type,
1801 MdastNodeType::Heading as u8
1802 );
1803 assert_eq!(decode_heading_data(arena.get_type_data(heading)).depth, 2);
1804 let children = arena.get_children(heading).to_vec();
1805 assert_eq!(children.len(), 1);
1806 assert_eq!(arena.get_node(children[0]).node_type, REF_NODE_TYPE);
1807 assert_eq!(
1808 u32::from_le_bytes(arena.get_type_data(children[0]).try_into().unwrap()),
1809 orig_text
1810 );
1811 }
1812
1813 #[test]
1814 fn opstream_ref_rejects_out_of_range_id() {
1815 let orig = test_parse_markdown("Hello");
1818 let bad = orig.len() as u32 + 100;
1819
1820 let mut ops = Vec::new();
1821 op_open(&mut ops, MdastNodeType::Paragraph);
1822 ops.push(OP_REF);
1823 ops.extend_from_slice(&bad.to_le_bytes());
1824 op_close(&mut ops);
1825
1826 let err = replay_mdast_for_test(&ops, &orig, 0).unwrap_err();
1827 assert!(matches!(err, CommandError::InvalidNodeId(id) if id == bad));
1828 }
1829
1830 #[test]
1831 fn opstream_rejects_nested_root() {
1832 let empty = ArenaBuilder::<Mdast>::new(String::new()).finish();
1833
1834 let mut ok = Vec::new();
1836 op_open(&mut ok, MdastNodeType::Root);
1837 op_close(&mut ok);
1838 assert!(replay_mdast_for_test(&ok, &empty, 0).is_ok());
1839
1840 let mut ops = Vec::new();
1841 op_open(&mut ops, MdastNodeType::Root);
1842 op_open(&mut ops, MdastNodeType::Root);
1843 let err = replay_mdast_for_test(&ops, &empty, 0).unwrap_err();
1844 assert!(matches!(err, CommandError::UnencodableNodeType("root")));
1845 }
1846
1847 #[test]
1848 fn hast_opstream_rejects_doctype() {
1849 let empty = ArenaBuilder::<Hast>::new(String::new()).finish();
1850 let ops = vec![OP_OPEN, HastNodeType::Doctype as u8, OP_CLOSE];
1851 let err = replay_hast_for_test(&ops, &empty, 0).unwrap_err();
1852 assert!(matches!(err, CommandError::UnencodableNodeType("doctype")));
1853 }
1854
1855 #[test]
1856 fn opstream_rejects_over_deep_nesting() {
1857 let empty = ArenaBuilder::<Mdast>::new(String::new()).finish();
1860 let mut ops = Vec::new();
1861 for _ in 0..(MAX_OPSTREAM_DEPTH + 1) {
1862 op_open(&mut ops, MdastNodeType::Blockquote);
1863 }
1864 let err = replay_mdast_for_test(&ops, &empty, 0).unwrap_err();
1865 assert!(matches!(err, CommandError::OpstreamTooDeep(_)));
1866 }
1867
1868 #[test]
1869 fn set_property_rejects_out_of_range_or_unparseable_int() {
1870 let heading_id = 1;
1872
1873 let mut buf = Vec::new();
1874 push_set_property(&mut buf, heading_id, PROP_INT, "depth", "9999");
1875 let err =
1876 apply_mdast_commands(build_hello_world(), &buf, &test_parse_markdown).unwrap_err();
1877 assert!(matches!(err, CommandError::PropertyValueOutOfRange { .. }));
1878
1879 let mut buf = Vec::new();
1880 push_set_property(&mut buf, heading_id, PROP_INT, "depth", "not-a-number");
1881 let err =
1882 apply_mdast_commands(build_hello_world(), &buf, &test_parse_markdown).unwrap_err();
1883 assert!(matches!(err, CommandError::PropertyValueOutOfRange { .. }));
1884
1885 let mut buf = Vec::new();
1887 push_set_property(&mut buf, heading_id, PROP_INT, "depth", "255");
1888 let arena = apply_mdast_commands(build_hello_world(), &buf, &test_parse_markdown).unwrap();
1889 assert_eq!(
1890 decode_heading_data(arena.get_type_data(heading_id)).depth,
1891 255
1892 );
1893 }
1894
1895 #[cfg(feature = "mdx")]
1896 #[test]
1897 fn mdx_jsx_set_property_replaces_named_attrs_and_keeps_explicit() {
1898 use satteri_ast::shared::MDX_ATTR_EXPRESSION_PROP;
1899
1900 let mut b = ArenaBuilder::<Hast>::new(String::new());
1901 b.open_node(HastNodeType::Root as u8);
1902 b.open_node(HastNodeType::MdxJsxElement as u8);
1903 let elem_name = b.alloc_string("Box");
1904 let foo = b.alloc_string("foo");
1905 let expr = b.alloc_string("1+1");
1906 let rest = b.alloc_string("rest");
1907 let attrs = vec![
1908 (MDX_ATTR_EXPRESSION_PROP, foo, expr),
1909 (MDX_ATTR_SPREAD, StringRef::empty(), rest),
1910 ];
1911 b.set_data_current(&encode_mdx_jsx_element_data(elem_name, &attrs, true));
1912 b.close_node();
1913 b.close_node();
1914 let mut arena = b.finish();
1915
1916 apply_hast_mdx_jsx_attribute(&mut arena, 1, "foo", PROP_STRING, "x").unwrap();
1919 let data = arena.get_type_data(1).to_vec();
1920 assert_eq!(decode_mdx_jsx_attr_count(&data), 2);
1921 assert!(decode_mdx_jsx_explicit(&data));
1922 let (k0, _, _) = decode_mdx_jsx_attr(&data, 0);
1923 assert_eq!(k0, MDX_ATTR_SPREAD);
1924 let (k1, n1, v1) = decode_mdx_jsx_attr(&data, 1);
1925 assert_eq!(k1, MDX_ATTR_LITERAL_PROP);
1926 assert_eq!(arena.get_str(n1), "foo");
1927 assert_eq!(arena.get_str(v1), "x");
1928
1929 apply_hast_mdx_jsx_attribute(&mut arena, 1, "id", PROP_STRING, "intro").unwrap();
1931 let data = arena.get_type_data(1).to_vec();
1932 assert_eq!(decode_mdx_jsx_attr_count(&data), 3);
1933 assert!(decode_mdx_jsx_explicit(&data));
1934 }
1935
1936 fn test_parse_markdown(source: &str) -> Arena<Mdast> {
1937 let mut b = ArenaBuilder::<Mdast>::new(String::new());
1938 b.open_node(MdastNodeType::Root as u8);
1939 b.open_node(MdastNodeType::Paragraph as u8);
1940 b.open_node(MdastNodeType::Text as u8);
1941 let sref = b.alloc_string(source);
1942 b.set_data_current(&satteri_arena::encode_string_ref_data(sref));
1943 b.close_node();
1944 b.close_node();
1945 b.close_node();
1946 b.finish()
1947 }
1948
1949 fn push_u32(buf: &mut Vec<u8>, v: u32) {
1950 buf.extend_from_slice(&v.to_le_bytes());
1951 }
1952
1953 fn push_set_property(buf: &mut Vec<u8>, node_id: u32, value_type: u8, name: &str, value: &str) {
1955 buf.push(CMD_SET_PROPERTY);
1956 push_u32(buf, node_id);
1957 buf.push(value_type);
1958 push_u32(buf, name.len() as u32);
1959 buf.extend_from_slice(name.as_bytes());
1960 push_u32(buf, value.len() as u32);
1961 buf.extend_from_slice(value.as_bytes());
1962 }
1963
1964 fn build_hello_world() -> Arena<Mdast> {
1965 use satteri_ast::mdast::codec::{encode_heading_data, encode_string_ref_data};
1966
1967 let source = "# Hello\n\nWorld".to_string();
1968 let mut b = ArenaBuilder::<Mdast>::new(source);
1969
1970 b.open_node(MdastNodeType::Root as u8);
1971 b.set_position_current(0, 14, 1, 1, 2, 6);
1972
1973 b.open_node(MdastNodeType::Heading as u8);
1974 b.set_position_current(0, 7, 1, 1, 1, 8);
1975 b.set_data_current(&encode_heading_data(1));
1976
1977 b.open_node(MdastNodeType::Text as u8);
1978 b.set_position_current(2, 7, 1, 3, 1, 8);
1979 b.set_data_current(&encode_string_ref_data(StringRef::new(2, 5)));
1980 b.close_node();
1981
1982 b.close_node();
1983
1984 b.open_node(MdastNodeType::Paragraph as u8);
1985 b.set_position_current(9, 14, 2, 1, 2, 6);
1986
1987 b.open_node(MdastNodeType::Text as u8);
1988 b.set_position_current(9, 14, 2, 1, 2, 6);
1989 b.set_data_current(&encode_string_ref_data(StringRef::new(9, 5)));
1990 b.close_node();
1991
1992 b.close_node();
1993 b.close_node();
1994
1995 b.finish()
1996 }
1997
1998 #[test]
1999 fn empty_command_buffer() {
2000 let arena = build_hello_world();
2001 let result = apply_mdast_commands(arena.clone(), &[], &test_parse_markdown).unwrap();
2002 assert_eq!(result.len(), arena.len());
2003 }
2004
2005 #[test]
2006 fn remove_command() {
2007 let arena = build_hello_world();
2008 let heading_id = arena.get_children(0)[0];
2009 let mut buf = Vec::new();
2010 buf.push(CMD_REMOVE);
2011 push_u32(&mut buf, heading_id);
2012
2013 let result = apply_mdast_commands(arena.clone(), &buf, &test_parse_markdown).unwrap();
2014 assert_eq!(result.get_children(0).len(), 1);
2015 assert_eq!(
2016 result.get_node(result.get_children(0)[0]).node_type,
2017 MdastNodeType::Paragraph as u8
2018 );
2019 }
2020
2021 #[test]
2022 fn set_property_heading_depth() {
2023 let arena = build_hello_world();
2024 let heading_id = arena.get_children(0)[0];
2025
2026 let mut buf = Vec::new();
2027 push_set_property(&mut buf, heading_id, PROP_INT, "depth", "3");
2028
2029 let result = apply_mdast_commands(arena.clone(), &buf, &test_parse_markdown).unwrap();
2030 let heading_data = result.get_type_data(heading_id);
2031 let heading = decode_heading_data(heading_data);
2032 assert_eq!(heading.depth, 3);
2033 }
2034
2035 #[test]
2036 fn set_property_text_value() {
2037 let arena = build_hello_world();
2038 let heading_id = arena.get_children(0)[0];
2039 let text_id = arena.get_children(heading_id)[0];
2040
2041 let mut buf = Vec::new();
2042 push_set_property(&mut buf, text_id, PROP_STRING, "value", "Goodbye");
2043
2044 let result = apply_mdast_commands(arena.clone(), &buf, &test_parse_markdown).unwrap();
2045 let text_data = result.get_type_data(text_id);
2046 let sref = decode_string_ref_data(text_data);
2047 assert_eq!(result.get_str(sref), "Goodbye");
2048 }
2049
2050 #[test]
2051 fn replace_with_raw_markdown() {
2052 let arena = build_hello_world();
2053 let heading_id = arena.get_children(0)[0];
2054
2055 let raw_md = "## New Heading";
2056 let mut buf = Vec::new();
2057 buf.push(CMD_REPLACE);
2058 push_u32(&mut buf, heading_id);
2059 buf.push(PAYLOAD_RAW);
2060 buf.push(0); push_u32(&mut buf, raw_md.len() as u32);
2062 buf.extend_from_slice(raw_md.as_bytes());
2063
2064 let result = apply_mdast_commands(arena.clone(), &buf, &test_parse_markdown).unwrap();
2065 let root_children = result.get_children(0);
2066 assert!(root_children.len() >= 2);
2067 }
2068
2069 #[test]
2070 fn stale_anchor_in_grafted_orphan_range_errors() {
2071 let arena = build_hello_world();
2072 let original_len = arena.len() as u32;
2073
2074 let mut ops = Vec::new();
2075 op_open(&mut ops, MdastNodeType::Paragraph);
2076 op_close(&mut ops);
2077
2078 let mut buf = Vec::new();
2080 buf.push(CMD_APPEND_CHILD);
2081 push_u32(&mut buf, 0);
2082 buf.push(PAYLOAD_OPSTREAM);
2083 push_u32(&mut buf, ops.len() as u32);
2084 buf.extend_from_slice(&ops);
2085 buf.push(CMD_REMOVE);
2086 push_u32(&mut buf, original_len);
2087
2088 let err = apply_mdast_commands(arena, &buf, &test_parse_markdown).unwrap_err();
2089 assert!(matches!(err, CommandError::InvalidNodeId(id) if id == original_len));
2090 }
2091
2092 #[test]
2093 fn multiple_commands() {
2094 let arena = build_hello_world();
2095 let heading_id = arena.get_children(0)[0];
2096 let text_id = arena.get_children(heading_id)[0];
2097
2098 let mut buf = Vec::new();
2099 push_set_property(&mut buf, heading_id, PROP_INT, "depth", "3");
2100 push_set_property(&mut buf, text_id, PROP_STRING, "value", "Hi");
2101
2102 let result = apply_mdast_commands(arena.clone(), &buf, &test_parse_markdown).unwrap();
2103
2104 let heading_data = result.get_type_data(heading_id);
2105 assert_eq!(decode_heading_data(heading_data).depth, 3);
2106
2107 let text_data = result.get_type_data(text_id);
2108 let sref = decode_string_ref_data(text_data);
2109 assert_eq!(result.get_str(sref), "Hi");
2110 }
2111
2112 #[test]
2113 fn set_property_null() {
2114 let arena = build_hello_world();
2115 let heading_id = arena.get_children(0)[0];
2116 let text_id = arena.get_children(heading_id)[0];
2117
2118 let mut buf = Vec::new();
2119 push_set_property(&mut buf, text_id, PROP_NULL, "value", "");
2120
2121 let result = apply_mdast_commands(arena.clone(), &buf, &test_parse_markdown).unwrap();
2122 let text_data = result.get_type_data(text_id);
2123 let sref = decode_string_ref_data(text_data);
2124 assert_eq!(sref.len, 0);
2125 }
2126
2127 #[test]
2128 fn set_property_invalid_field_reports_property_and_node_type() {
2129 let arena = build_hello_world();
2130 let heading_id = arena.get_children(0)[0];
2131
2132 let mut buf = Vec::new();
2133 push_set_property(&mut buf, heading_id, PROP_STRING, "value", "x");
2134
2135 let err = apply_mdast_commands(arena, &buf, &test_parse_markdown).unwrap_err();
2136 assert!(matches!(
2137 err,
2138 CommandError::UnknownField { ref name, ref node_type }
2139 if name == "value" && node_type == "heading"
2140 ));
2141 assert_eq!(
2142 err.to_string(),
2143 "cannot set property 'value' on a 'heading' node"
2144 );
2145 }
2146
2147 #[test]
2148 fn set_property_wrong_value_type_reports_value_mismatch() {
2149 let arena = build_hello_world();
2150 let heading_id = arena.get_children(0)[0];
2151
2152 let mut buf = Vec::new();
2154 push_set_property(&mut buf, heading_id, PROP_STRING, "depth", "3");
2155
2156 let err = apply_mdast_commands(arena, &buf, &test_parse_markdown).unwrap_err();
2157 assert!(matches!(
2158 err,
2159 CommandError::InvalidPropertyValue { ref name, ref node_type }
2160 if name == "depth" && node_type == "heading"
2161 ));
2162 assert_eq!(
2163 err.to_string(),
2164 "property 'depth' on a 'heading' node cannot hold a value of this type"
2165 );
2166 }
2167
2168 fn build_single_node(node_type: MdastNodeType, type_data: &[u8]) -> Arena<Mdast> {
2170 let mut b = ArenaBuilder::<Mdast>::new(String::new());
2171 b.open_node(MdastNodeType::Root as u8);
2172 b.open_node(node_type as u8);
2173 b.set_data_current(type_data);
2174 b.close_node();
2175 b.close_node();
2176 b.finish()
2177 }
2178
2179 #[test]
2180 fn set_property_image_reference_alt_roundtrip() {
2181 let mut b = ArenaBuilder::<Mdast>::new(String::new());
2182 b.open_node(MdastNodeType::Root as u8);
2183 b.open_node(MdastNodeType::ImageReference as u8);
2184 let identifier = b.alloc_string("img");
2185 let alt = b.alloc_string("old");
2186 b.set_data_current(&encode_image_reference_data(identifier, identifier, 0, alt));
2187 b.close_node();
2188 b.close_node();
2189 let arena = b.finish();
2190 let image_ref_id = arena.get_children(0)[0];
2191
2192 let mut buf = Vec::new();
2193 push_set_property(&mut buf, image_ref_id, PROP_STRING, "alt", "new alt");
2194
2195 let result = apply_mdast_commands(arena, &buf, &test_parse_markdown).unwrap();
2196 let alt = decode_image_reference_alt(result.get_type_data(image_ref_id));
2197 assert_eq!(result.get_str(alt), "new alt");
2198 }
2199
2200 #[test]
2201 fn set_property_reference_type_valid_and_invalid() {
2202 let mut b = ArenaBuilder::<Mdast>::new(String::new());
2203 b.open_node(MdastNodeType::Root as u8);
2204 b.open_node(MdastNodeType::LinkReference as u8);
2205 let identifier = b.alloc_string("ref");
2206 b.set_data_current(&encode_reference_data(identifier, identifier, 0));
2207 b.close_node();
2208 b.close_node();
2209 let arena = b.finish();
2210 let link_ref_id = arena.get_children(0)[0];
2211
2212 let mut buf = Vec::new();
2213 push_set_property(&mut buf, link_ref_id, PROP_STRING, "referenceType", "full");
2214 let result = apply_mdast_commands(arena.clone(), &buf, &test_parse_markdown).unwrap();
2215 let reference = decode_reference_data(result.get_type_data(link_ref_id));
2216 assert_eq!(reference.reference_kind, 2);
2217
2218 let mut buf = Vec::new();
2220 push_set_property(&mut buf, link_ref_id, PROP_STRING, "referenceType", "bogus");
2221 let err = apply_mdast_commands(arena, &buf, &test_parse_markdown).unwrap_err();
2222 assert!(matches!(
2223 err,
2224 CommandError::InvalidPropertyValue { ref name, ref node_type }
2225 if name == "referenceType" && node_type == "linkReference"
2226 ));
2227 }
2228
2229 #[test]
2230 fn set_property_list_start_and_ordered() {
2231 let arena = build_single_node(MdastNodeType::List, &encode_list_data(false, 1, false));
2232 let list_id = arena.get_children(0)[0];
2233
2234 let mut buf = Vec::new();
2235 push_set_property(&mut buf, list_id, PROP_INT, "start", "5");
2236 push_set_property(&mut buf, list_id, PROP_BOOL_TRUE, "ordered", "");
2237
2238 let result = apply_mdast_commands(arena, &buf, &test_parse_markdown).unwrap();
2239 let list = decode_list_data(result.get_type_data(list_id));
2240 assert_eq!(list.start, 5);
2241 assert!(list.ordered);
2242 assert!(!list.spread);
2243 }
2244
2245 #[test]
2246 fn escape_braces_in_html_text_basic() {
2247 assert_eq!(
2248 escape_braces_in_html_text("<span>{foo: 1}</span>"),
2249 "<span>{'{'}foo: 1{'}'}</span>"
2250 );
2251 }
2252
2253 #[test]
2254 fn escape_braces_after_comparison_less_than() {
2255 assert_eq!(
2256 escape_braces_in_html_text("<span>5 < 6 and {literal} here</span>"),
2257 "<span>5 < 6 and {'{'}literal{'}'} here</span>"
2258 );
2259 assert_eq!(
2260 escape_braces_in_html_text("a < b {notExpr} tail"),
2261 "a < b {'{'}notExpr{'}'} tail"
2262 );
2263 assert_eq!(
2264 escape_braces_in_html_text("trailing {x} <"),
2265 "trailing {'{'}x{'}'} <"
2266 );
2267 }
2268
2269 #[test]
2270 fn escape_braces_preserves_attributes() {
2271 let result = escape_braces_in_html_text(r#"<span data-x="{a}">{b}</span>"#);
2272 assert!(
2273 result.contains(r#"data-x="{a}""#),
2274 "attribute braces preserved"
2275 );
2276 assert!(result.contains("{'{'}"), "text braces escaped");
2277 }
2278
2279 #[test]
2280 fn escape_braces_no_braces() {
2281 let html = r#"<pre class="shiki"><code><span style="color:red">hello</span></code></pre>"#;
2282 assert_eq!(escape_braces_in_html_text(html), html);
2283 }
2284
2285 #[test]
2286 fn escape_braces_shiki_output() {
2287 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>"#;
2288 let escaped = escape_braces_in_html_text(html);
2289 assert!(
2290 !escaped.contains(">{<"),
2291 "bare braces in text should be escaped"
2292 );
2293 assert!(
2294 !escaped.contains(">}<"),
2295 "bare braces in text should be escaped"
2296 );
2297 assert!(escaped.contains(r#"class="shiki""#));
2298 assert!(escaped.contains(r#"style="color:#E1E4E8""#));
2299 }
2300
2301 #[test]
2302 fn hast_set_property_add_new() {
2303 let arena = build_hast_element(&[]);
2304 let element_id = arena.get_children(0)[0];
2305
2306 let mut buf = Vec::new();
2307 push_set_property(&mut buf, element_id, PROP_STRING, "class", "test");
2308
2309 let result = apply_hast_commands(arena.clone(), &buf).unwrap();
2310 let data = result.get_type_data(element_id);
2311 let prop_count = u32::from_le_bytes(data[8..12].try_into().unwrap());
2312 assert_eq!(prop_count, 1);
2313 let name_ref = StringRef::new(
2314 u32::from_le_bytes(data[16..20].try_into().unwrap()),
2315 u32::from_le_bytes(data[20..24].try_into().unwrap()),
2316 );
2317 assert_eq!(result.get_str(name_ref), "class");
2318 let val_ref = StringRef::new(
2319 u32::from_le_bytes(data[28..32].try_into().unwrap()),
2320 u32::from_le_bytes(data[32..36].try_into().unwrap()),
2321 );
2322 assert_eq!(result.get_str(val_ref), "test");
2323 assert_eq!(data[24], PROP_STRING);
2324 }
2325
2326 #[test]
2327 fn hast_set_property_overwrite_existing() {
2328 let arena = build_hast_element(&[("class", PROP_STRING, "old")]);
2329 let element_id = arena.get_children(0)[0];
2330
2331 let mut buf = Vec::new();
2332 push_set_property(&mut buf, element_id, PROP_STRING, "class", "new-value");
2333
2334 let result = apply_hast_commands(arena.clone(), &buf).unwrap();
2335 let data = result.get_type_data(element_id);
2336 let prop_count = u32::from_le_bytes(data[8..12].try_into().unwrap());
2337 assert_eq!(prop_count, 1);
2338 let val_ref = StringRef::new(
2339 u32::from_le_bytes(data[28..32].try_into().unwrap()),
2340 u32::from_le_bytes(data[32..36].try_into().unwrap()),
2341 );
2342 assert_eq!(result.get_str(val_ref), "new-value");
2343 }
2344
2345 #[test]
2346 fn hast_set_property_bool_true() {
2347 let arena = build_hast_element(&[]);
2348 let element_id = arena.get_children(0)[0];
2349
2350 let mut buf = Vec::new();
2351 push_set_property(&mut buf, element_id, PROP_BOOL_TRUE, "disabled", "");
2352
2353 let result = apply_hast_commands(arena.clone(), &buf).unwrap();
2354 let data = result.get_type_data(element_id);
2355 let prop_count = u32::from_le_bytes(data[8..12].try_into().unwrap());
2356 assert_eq!(prop_count, 1);
2357 assert_eq!(data[24], PROP_BOOL_TRUE);
2358 }
2359
2360 #[test]
2361 fn hast_set_property_multiple_on_same_node() {
2362 let arena = build_hast_element(&[]);
2363 let element_id = arena.get_children(0)[0];
2364
2365 let mut buf = Vec::new();
2366 push_set_property(&mut buf, element_id, PROP_STRING, "class", "foo");
2367 push_set_property(&mut buf, element_id, PROP_STRING, "id", "bar");
2368
2369 let result = apply_hast_commands(arena.clone(), &buf).unwrap();
2370 let data = result.get_type_data(element_id);
2371 let prop_count = u32::from_le_bytes(data[8..12].try_into().unwrap());
2372 assert_eq!(prop_count, 2);
2373 }
2374
2375 fn build_hast_element(props: &[(&str, u8, &str)]) -> Arena<Hast> {
2377 use satteri_ast::hast::node::HastNodeType;
2378
2379 let mut b = ArenaBuilder::<Hast>::new(String::new());
2380 b.open_node_raw(HastNodeType::Root as u8);
2381 b.open_node_raw(HastNodeType::Element as u8);
2382 let tag_ref = b.alloc_string("div");
2383 let prop_tuples: Vec<(StringRef, u8, StringRef)> = props
2384 .iter()
2385 .map(|(name, kind, value)| {
2386 let n = b.alloc_string(name);
2387 let v = if value.is_empty() {
2388 StringRef::empty()
2389 } else {
2390 b.alloc_string(value)
2391 };
2392 (n, *kind, v)
2393 })
2394 .collect();
2395 let mut type_data = Vec::with_capacity(16 + prop_tuples.len() * 20);
2396 type_data.extend_from_slice(&tag_ref.offset.to_le_bytes());
2397 type_data.extend_from_slice(&tag_ref.len.to_le_bytes());
2398 type_data.extend_from_slice(&(prop_tuples.len() as u32).to_le_bytes());
2399 type_data.extend_from_slice(&0u32.to_le_bytes());
2400 for (n, kind, v) in &prop_tuples {
2401 type_data.extend_from_slice(&n.offset.to_le_bytes());
2402 type_data.extend_from_slice(&n.len.to_le_bytes());
2403 type_data.push(*kind);
2404 type_data.extend_from_slice(&[0u8; 3]);
2405 type_data.extend_from_slice(&v.offset.to_le_bytes());
2406 type_data.extend_from_slice(&v.len.to_le_bytes());
2407 }
2408 b.set_data_current(&type_data);
2409 b.close_node();
2410 b.close_node();
2411 b.finish()
2412 }
2413
2414 fn push_raw_command(buf: &mut Vec<u8>, cmd: u8, node_id: u32, raw: &str) {
2415 buf.push(cmd);
2416 push_u32(buf, node_id);
2417 buf.push(PAYLOAD_RAW);
2418 buf.push(0); push_u32(buf, raw.len() as u32);
2420 buf.extend_from_slice(raw.as_bytes());
2421 }
2422
2423 fn parse_two_blocks(_source: &str) -> Arena<Mdast> {
2424 let mut b = ArenaBuilder::<Mdast>::new(String::new());
2425 b.open_node(MdastNodeType::Root as u8);
2426 b.open_node(MdastNodeType::Paragraph as u8);
2427 b.close_node();
2428 b.open_node(MdastNodeType::Paragraph as u8);
2429 b.close_node();
2430 b.close_node();
2431 b.finish()
2432 }
2433
2434 fn parse_leaf_block(_source: &str) -> Arena<Mdast> {
2435 let mut b = ArenaBuilder::<Mdast>::new(String::new());
2436 b.open_node(MdastNodeType::Root as u8);
2437 b.open_node(MdastNodeType::ThematicBreak as u8);
2438 b.close_node();
2439 b.close_node();
2440 b.finish()
2441 }
2442
2443 #[test]
2444 fn mdast_wrap_with_raw_payload() {
2445 let arena = build_hello_world();
2446 let heading_id = arena.get_children(0)[0];
2447 let mut buf = Vec::new();
2448 push_raw_command(&mut buf, CMD_WRAP, heading_id, "quoted");
2449
2450 let result = apply_mdast_commands(arena, &buf, &test_parse_markdown).unwrap();
2451 let wrapper = result.get_children(0)[0];
2452 assert_eq!(
2453 result.get_node(wrapper).node_type,
2454 MdastNodeType::Paragraph as u8
2455 );
2456 let wrapped = result.get_children(wrapper);
2457 assert_eq!(wrapped.len(), 2);
2458 assert_eq!(
2459 result.get_node(wrapped[0]).node_type,
2460 MdastNodeType::Heading as u8
2461 );
2462 assert_eq!(
2463 result.get_node(wrapped[1]).node_type,
2464 MdastNodeType::Text as u8
2465 );
2466 }
2467
2468 #[test]
2469 fn mdast_wrap_with_multi_block_raw_payload_errors() {
2470 let arena = build_hello_world();
2471 let heading_id = arena.get_children(0)[0];
2472 let mut buf = Vec::new();
2473 push_raw_command(&mut buf, CMD_WRAP, heading_id, "one\n\ntwo");
2474
2475 let err = apply_mdast_commands(arena, &buf, &parse_two_blocks).unwrap_err();
2476 assert!(
2477 matches!(&err, CommandError::InvalidRawWrapper(r) if r.contains("exactly one block")),
2478 "{err:?}"
2479 );
2480 }
2481
2482 #[test]
2483 fn mdast_wrap_with_leaf_raw_payload_errors() {
2484 let arena = build_hello_world();
2485 let heading_id = arena.get_children(0)[0];
2486 let mut buf = Vec::new();
2487 push_raw_command(&mut buf, CMD_WRAP, heading_id, "---");
2488
2489 let err = apply_mdast_commands(arena, &buf, &parse_leaf_block).unwrap_err();
2490 assert!(
2491 matches!(&err, CommandError::InvalidRawWrapper(r) if r.contains("thematicBreak")),
2492 "{err:?}"
2493 );
2494 }
2495
2496 #[cfg(feature = "from-html")]
2497 #[test]
2498 fn hast_wrap_with_raw_html_payload() {
2499 let arena = build_hast_element(&[]);
2500 let mut buf = Vec::new();
2501 push_raw_command(&mut buf, CMD_WRAP, 1, "<section class=\"wrap\"></section>");
2502
2503 let result = apply_hast_commands(arena, &buf).unwrap();
2504 let section = result.get_children(0)[0];
2505 assert_eq!(
2506 result.get_str(decode_element_tag(result.get_type_data(section))),
2507 "section"
2508 );
2509 let wrapped = result.get_children(section);
2510 assert_eq!(wrapped.len(), 1);
2511 assert_eq!(
2512 result.get_str(decode_element_tag(result.get_type_data(wrapped[0]))),
2513 "div"
2514 );
2515 }
2516
2517 #[cfg(feature = "from-html")]
2518 #[test]
2519 fn hast_wrap_with_invalid_raw_html_errors() {
2520 let arena = build_hast_element(&[]);
2521 let mut buf = Vec::new();
2522 push_raw_command(&mut buf, CMD_WRAP, 1, "just text");
2523
2524 let err = apply_hast_commands(arena, &buf).unwrap_err();
2525 assert!(matches!(err, CommandError::InvalidRawWrapper(_)), "{err:?}");
2526 }
2527
2528 fn push_element_wrap_command(buf: &mut Vec<u8>, node_id: u32, tag: &str) {
2530 let mut ops = Vec::new();
2531 ops.push(OP_OPEN);
2532 ops.push(HastNodeType::Element as u8);
2533 op_str(&mut ops, OF_TAGNAME, tag);
2534 ops.push(OP_CLOSE);
2535
2536 buf.push(CMD_WRAP);
2537 push_u32(buf, node_id);
2538 buf.push(PAYLOAD_OPSTREAM);
2539 push_u32(buf, ops.len() as u32);
2540 buf.extend_from_slice(&ops);
2541 }
2542
2543 #[test]
2544 fn hast_wrap_with_void_element_payload_errors() {
2545 let arena = build_hast_element(&[]);
2546 let mut buf = Vec::new();
2547 push_element_wrap_command(&mut buf, 1, "img");
2548
2549 let err = apply_hast_commands(arena, &buf).unwrap_err();
2550 assert!(
2551 matches!(&err, CommandError::VoidWrapParent(tag) if tag == "img"),
2552 "{err:?}"
2553 );
2554 }
2555
2556 #[test]
2557 fn hast_wrap_with_element_payload_wraps() {
2558 let arena = build_hast_element(&[]);
2559 let mut buf = Vec::new();
2560 push_element_wrap_command(&mut buf, 1, "section");
2561
2562 let result = apply_hast_commands(arena, &buf).unwrap();
2563 let section = result.get_children(0)[0];
2564 assert_eq!(
2565 result.get_str(decode_element_tag(result.get_type_data(section))),
2566 "section"
2567 );
2568 let wrapped = result.get_children(section);
2569 assert_eq!(wrapped.len(), 1);
2570 assert_eq!(
2571 result.get_str(decode_element_tag(result.get_type_data(wrapped[0]))),
2572 "div"
2573 );
2574 }
2575
2576 #[test]
2577 fn hast_raw_payload_rejected_outside_wrap() {
2578 let arena = build_hast_element(&[]);
2579 let mut buf = Vec::new();
2580 push_raw_command(&mut buf, CMD_INSERT_BEFORE, 1, "<span></span>");
2581
2582 let err = apply_hast_commands(arena, &buf).unwrap_err();
2583 assert!(
2584 matches!(err, CommandError::UnknownPayloadType(p) if p == PAYLOAD_RAW),
2585 "{err:?}"
2586 );
2587 }
2588}