1use satteri_arena::{Arena, ArenaBuilder, ArenaKind, Hast, Mdast, StringRef};
42use satteri_ast::commands::{CommandError, JsNode};
43use satteri_ast::hast::HastNodeType;
44use satteri_ast::mdast::codec::*;
45use satteri_ast::mdast::MdastNodeType;
46use satteri_ast::rebuild::{Patch, REF_NODE_TYPE};
47#[cfg(feature = "mdx")]
48use satteri_ast::shared::encode_js_jsx_attrs;
49use satteri_ast::shared::{
50 PROP_BOOL_FALSE, PROP_BOOL_TRUE, PROP_INT, PROP_NULL, PROP_SPACE_SEP, PROP_STRING,
51};
52
53const CMD_REMOVE: u8 = 0x01;
55const CMD_INSERT_BEFORE: u8 = 0x05;
56const CMD_INSERT_AFTER: u8 = 0x06;
57const CMD_PREPEND_CHILD: u8 = 0x07;
58const CMD_APPEND_CHILD: u8 = 0x08;
59const CMD_WRAP: u8 = 0x09;
60const CMD_REPLACE: u8 = 0x0B;
61const CMD_SET_PROPERTY: u8 = 0x0C;
62const CMD_SET_CHILDREN: u8 = 0x0D;
63
64const PAYLOAD_RAW_MARKDOWN: u8 = 0x10;
65const PAYLOAD_RAW_HTML: u8 = 0x11;
66const PAYLOAD_SERDE_JSON: u8 = 0x12;
67
68const FIELD_DEPTH: u16 = 0x0001;
70const FIELD_URL: u16 = 0x0010;
71const FIELD_TITLE: u16 = 0x0011;
72const FIELD_LANG: u16 = 0x0020;
73const FIELD_META: u16 = 0x0021;
74const FIELD_VALUE: u16 = 0x0022;
75const FIELD_ALT: u16 = 0x0030;
76const FIELD_ORDERED: u16 = 0x0040;
77const FIELD_START: u16 = 0x0041;
78const FIELD_SPREAD: u16 = 0x0042;
79const FIELD_CHECKED: u16 = 0x0050;
80const FIELD_IDENTIFIER: u16 = 0x0060;
81const FIELD_LABEL: u16 = 0x0061;
82const FIELD_REFERENCE_TYPE: u16 = 0x0062;
83const FIELD_NAME: u16 = 0x0070;
84
85struct BufReader<'a> {
86 data: &'a [u8],
87 pos: usize,
88}
89
90impl<'a> BufReader<'a> {
91 fn new(data: &'a [u8]) -> Self {
92 Self { data, pos: 0 }
93 }
94
95 fn remaining(&self) -> usize {
96 self.data.len() - self.pos
97 }
98
99 fn read_u8(&mut self) -> Result<u8, CommandError> {
100 if self.remaining() < 1 {
101 return Err(CommandError::UnexpectedEof);
102 }
103 let v = self.data[self.pos];
104 self.pos += 1;
105 Ok(v)
106 }
107
108 fn read_u32(&mut self) -> Result<u32, CommandError> {
109 if self.remaining() < 4 {
110 return Err(CommandError::UnexpectedEof);
111 }
112 let v = u32::from_le_bytes([
113 self.data[self.pos],
114 self.data[self.pos + 1],
115 self.data[self.pos + 2],
116 self.data[self.pos + 3],
117 ]);
118 self.pos += 4;
119 Ok(v)
120 }
121
122 fn read_bytes(&mut self, len: usize) -> Result<&'a [u8], CommandError> {
123 if self.remaining() < len {
124 return Err(CommandError::UnexpectedEof);
125 }
126 let slice = &self.data[self.pos..self.pos + len];
127 self.pos += len;
128 Ok(slice)
129 }
130
131 fn read_str(&mut self, len: usize) -> Result<&'a str, CommandError> {
132 let bytes = self.read_bytes(len)?;
133 std::str::from_utf8(bytes).map_err(|_| CommandError::InvalidUtf8)
134 }
135}
136
137#[cfg(feature = "mdx")]
140fn js_data_is_mdx_explicit(data: &Option<serde_json::Map<String, serde_json::Value>>) -> bool {
141 data.as_ref()
142 .and_then(|m| m.get("_mdxExplicitJsx"))
143 .and_then(serde_json::Value::as_bool)
144 .unwrap_or(false)
145}
146
147fn apply_data_property<K: ArenaKind>(
150 arena: &mut Arena<K>,
151 node_id: u32,
152 value_type: u8,
153 value_str: &str,
154) {
155 if value_type == PROP_NULL {
156 arena.set_node_data(node_id, Vec::new());
157 } else {
158 arena.set_node_data(node_id, value_str.as_bytes().to_vec());
159 }
160}
161
162fn resolve_mdast_field(node_type: u8, name: &str) -> Option<u16> {
164 match (node_type, name) {
165 (2, "depth") => Some(FIELD_DEPTH),
166 (8, "lang") => Some(FIELD_LANG),
167 (8, "meta") => Some(FIELD_META),
168 (8, "value") => Some(FIELD_VALUE),
169 (15, "url") => Some(FIELD_URL),
170 (15, "title") => Some(FIELD_TITLE),
171 (16, "url") => Some(FIELD_URL),
172 (16, "alt") => Some(FIELD_ALT),
173 (16, "title") => Some(FIELD_TITLE),
174 (10 | 13 | 7 | 25 | 26 | 28, "value") => Some(FIELD_VALUE),
175 (27, "meta") => Some(FIELD_META),
176 (27, "value") => Some(FIELD_VALUE),
177 (102..=104, "value") => Some(FIELD_VALUE),
178 (9, "url") => Some(FIELD_URL),
179 (9, "title") => Some(FIELD_TITLE),
180 (5, "ordered") => Some(FIELD_ORDERED),
181 (5, "start") => Some(FIELD_START),
182 (5 | 6, "spread") => Some(FIELD_SPREAD),
183 (6, "checked") => Some(FIELD_CHECKED),
184 (9 | 17 | 18 | 19 | 20, "identifier") => Some(FIELD_IDENTIFIER),
185 (9 | 17 | 18 | 19 | 20, "label") => Some(FIELD_LABEL),
186 (17 | 18 | 20, "referenceType") => Some(FIELD_REFERENCE_TYPE),
187 (100 | 101, "name") => Some(FIELD_NAME),
188 _ => None,
189 }
190}
191
192fn mdast_type_name(node_type: u8) -> String {
194 match MdastNodeType::from_u8(node_type) {
195 Some(t) => t.name().to_string(),
196 None => format!("unknown({node_type})"),
197 }
198}
199
200fn apply_mdast_set_property(
204 arena: &mut Arena<Mdast>,
205 node_id: u32,
206 prop_name: &str,
207 value_type: u8,
208 value_str: &str,
209) -> Result<(), CommandError> {
210 if prop_name == "data" {
211 apply_data_property(arena, node_id, value_type, value_str);
212 return Ok(());
213 }
214
215 let node_type = arena.get_node(node_id).node_type;
216
217 let field_id =
219 resolve_mdast_field(node_type, prop_name).ok_or_else(|| CommandError::UnknownField {
220 node_type: mdast_type_name(node_type),
221 name: prop_name.to_string(),
222 })?;
223
224 let written: Option<()> = match value_type {
227 PROP_STRING | PROP_SPACE_SEP => {
228 let sref = arena.alloc_string(value_str);
229 set_mdast_string_ref(arena, node_id, field_id, sref)
230 }
231 PROP_BOOL_TRUE => apply_mdast_bool(arena, node_id, node_type, field_id, true),
232 PROP_BOOL_FALSE => apply_mdast_bool(arena, node_id, node_type, field_id, false),
233 PROP_INT => {
234 let value: i64 = value_str.parse().unwrap_or(0);
235 apply_mdast_int(arena, node_id, node_type, field_id, value)
236 }
237 PROP_NULL => apply_mdast_null(arena, node_id, node_type, field_id),
238 _ => return Err(CommandError::UnknownCommand(value_type)),
239 };
240 written.ok_or_else(|| CommandError::InvalidPropertyValue {
241 node_type: mdast_type_name(node_type),
242 name: prop_name.to_string(),
243 })
244}
245
246fn apply_mdast_int(
247 arena: &mut Arena<Mdast>,
248 node_id: u32,
249 node_type: u8,
250 field_id: u16,
251 value: i64,
252) -> Option<()> {
253 let data_offset = arena.get_node(node_id).data_offset as usize;
254 let data_len = arena.get_node(node_id).data_len as usize;
255 match (node_type, field_id) {
256 (2, FIELD_DEPTH) => {
257 if data_len >= 1 {
258 arena.type_data[data_offset] = value as u8;
259 }
260 }
261 (5, FIELD_START) => {
262 if data_len >= 4 {
263 arena.type_data[data_offset..data_offset + 4]
264 .copy_from_slice(&(value as u32).to_ne_bytes());
265 }
266 }
267 (6, FIELD_CHECKED) => {
268 if data_len >= 1 {
269 arena.type_data[data_offset] = value as u8;
270 }
271 }
272 _ => return None,
273 }
274 Some(())
275}
276
277fn apply_mdast_bool(
278 arena: &mut Arena<Mdast>,
279 node_id: u32,
280 node_type: u8,
281 field_id: u16,
282 value: bool,
283) -> Option<()> {
284 let data_offset = arena.get_node(node_id).data_offset as usize;
285 let data_len = arena.get_node(node_id).data_len as usize;
286 match (node_type, field_id) {
287 (5, FIELD_ORDERED) => {
288 if data_len >= 5 {
289 arena.type_data[data_offset + 4] = value as u8;
290 }
291 }
292 (5, FIELD_SPREAD) => {
293 if data_len >= 6 {
294 arena.type_data[data_offset + 5] = value as u8;
295 }
296 }
297 (6, FIELD_SPREAD) => {
298 if data_len >= 2 {
299 arena.type_data[data_offset + 1] = value as u8;
300 }
301 }
302 _ => return None,
303 }
304 Some(())
305}
306
307fn apply_mdast_null(
308 arena: &mut Arena<Mdast>,
309 node_id: u32,
310 node_type: u8,
311 field_id: u16,
312) -> Option<()> {
313 match (node_type, field_id) {
314 (6, FIELD_CHECKED) => {
315 let data_offset = arena.get_node(node_id).data_offset as usize;
316 let data_len = arena.get_node(node_id).data_len as usize;
317 if data_len >= 1 {
318 arena.type_data[data_offset] = 2;
319 }
320 Some(())
321 }
322 _ => set_mdast_string_ref(arena, node_id, field_id, StringRef::empty()),
323 }
324}
325
326fn set_mdast_string_ref(
327 arena: &mut Arena<Mdast>,
328 node_id: u32,
329 field_id: u16,
330 sref: StringRef,
331) -> Option<()> {
332 let node = arena.get_node(node_id);
333 let node_type = node.node_type;
334 let data_offset = node.data_offset as usize;
335
336 let ref_offset = match (node_type, field_id) {
337 (10 | 13 | 7 | 25 | 26 | 28, FIELD_VALUE) => 0,
339 (15, FIELD_URL) => 0,
341 (15, FIELD_TITLE) => 8,
342 (16, FIELD_URL) => 0,
344 (16, FIELD_ALT) => 8,
345 (16, FIELD_TITLE) => 16,
346 (8, FIELD_LANG) => 0,
348 (8, FIELD_META) => 8,
349 (8, FIELD_VALUE) => 16,
350 (27, FIELD_META) => 0,
352 (27, FIELD_VALUE) => 8,
353 (9, FIELD_URL) => 0,
355 (9, FIELD_TITLE) => 8,
356 (9, FIELD_IDENTIFIER) => 16,
357 (9, FIELD_LABEL) => 24,
358 (17 | 18 | 20, FIELD_IDENTIFIER) => 0,
360 (17 | 18 | 20, FIELD_LABEL) => 8,
361 (19, FIELD_IDENTIFIER) => 0,
363 (19, FIELD_LABEL) => 8,
364 (100 | 101, FIELD_NAME) => 0,
366 (102..=104, FIELD_VALUE) => 0,
368 _ => return None,
369 };
370
371 let abs_offset = data_offset + ref_offset;
372 let bytes_offset = sref.offset.to_ne_bytes();
373 let bytes_len = sref.len.to_ne_bytes();
374 arena.type_data[abs_offset..abs_offset + 4].copy_from_slice(&bytes_offset);
375 arena.type_data[abs_offset + 4..abs_offset + 8].copy_from_slice(&bytes_len);
376
377 Some(())
378}
379
380fn parse_raw_markdown(
381 markdown: &str,
382 parse_markdown: &dyn Fn(&str) -> Arena<Mdast>,
383) -> Arena<Mdast> {
384 parse_markdown(markdown)
385}
386
387fn escape_braces_in_html_text(html: &str) -> String {
395 let mut result = String::with_capacity(html.len());
396 let mut in_tag = false;
397 let mut in_quote: Option<char> = None;
398
399 for ch in html.chars() {
400 if in_tag {
401 match ch {
402 '"' | '\'' if in_quote == Some(ch) => {
403 in_quote = None;
404 result.push(ch);
405 }
406 '"' | '\'' if in_quote.is_none() => {
407 in_quote = Some(ch);
408 result.push(ch);
409 }
410 '>' if in_quote.is_none() => {
411 in_tag = false;
412 result.push(ch);
413 }
414 _ => result.push(ch),
415 }
416 } else {
417 match ch {
418 '<' => {
419 in_tag = true;
420 result.push(ch);
421 }
422 '{' => result.push_str("{'{'}"),
423 '}' => result.push_str("{'}'}"),
424 _ => result.push(ch),
425 }
426 }
427 }
428 result
429}
430
431fn js_node_to_mdast_arena(js_node: &JsNode) -> Result<(Arena<Mdast>, bool), CommandError> {
432 if js_node.is_hast {
433 return Err(CommandError::UnknownNodeType(format!(
434 "expected mdast node, got hast-flagged `{}`",
435 js_node.node_type
436 )));
437 }
438 let mut builder = ArenaBuilder::<Mdast>::new(String::new());
439 emit_mdast_js_node(js_node, &mut builder)?;
440 Ok((builder.finish(), js_node.keep_children))
441}
442
443fn js_node_to_hast_arena(js_node: &JsNode) -> Result<(Arena<Hast>, bool), CommandError> {
444 if !js_node.is_hast {
445 return Err(CommandError::UnknownNodeType(format!(
446 "expected hast node, got mdast-flagged `{}`",
447 js_node.node_type
448 )));
449 }
450 let mut builder = ArenaBuilder::<Hast>::new(String::new());
451 emit_hast_js_node(js_node, &mut builder)?;
452 Ok((builder.finish(), js_node.keep_children))
453}
454
455fn emit_ref_node<K: ArenaKind>(ref_id: u32, builder: &mut ArenaBuilder<K>) {
459 builder.open_node_raw(REF_NODE_TYPE);
460 builder.set_data_current(&ref_id.to_le_bytes());
461 builder.close_node();
462}
463
464fn emit_mdast_js_node(
465 js_node: &JsNode,
466 builder: &mut ArenaBuilder<Mdast>,
467) -> Result<(), CommandError> {
468 if let Some(ref_id) = js_node.ref_id {
469 emit_ref_node(ref_id, builder);
470 return Ok(());
471 }
472
473 if js_node.is_hast {
474 return Err(CommandError::UnknownNodeType(format!(
475 "expected mdast node, got hast-flagged `{}`",
476 js_node.node_type
477 )));
478 }
479
480 let node_type = name_to_node_type(&js_node.node_type)?;
481 builder.open_node(node_type as u8);
482
483 let type_data = encode_js_node_data(js_node, node_type, builder);
484 if !type_data.is_empty() {
485 builder.set_data_current(&type_data);
486 }
487
488 write_js_node_data(js_node, builder)?;
489
490 if let Some(children) = &js_node.children {
491 for child in children {
492 emit_mdast_js_node(child, builder)?;
493 }
494 }
495
496 builder.close_node();
497 Ok(())
498}
499
500fn write_js_node_data<K: ArenaKind>(
501 js_node: &JsNode,
502 builder: &mut ArenaBuilder<K>,
503) -> Result<(), CommandError> {
504 let Some(data) = &js_node.data else {
505 return Ok(());
506 };
507 let id = builder.current_node_id();
508 let json = serde_json::to_vec(data).map_err(|e| CommandError::InvalidJson(e.to_string()))?;
509 builder.arena_mut().set_node_data(id, json);
510 Ok(())
511}
512
513fn encode_js_node_data(
514 js_node: &JsNode,
515 node_type: MdastNodeType,
516 builder: &mut ArenaBuilder<Mdast>,
517) -> Vec<u8> {
518 match node_type {
519 MdastNodeType::Heading => {
520 let depth = js_node.depth.unwrap_or(1);
521 encode_heading_data(depth)
522 }
523 MdastNodeType::Text
524 | MdastNodeType::InlineCode
525 | MdastNodeType::Html
526 | MdastNodeType::Yaml
527 | MdastNodeType::Toml => {
528 let value = js_node.value.as_deref().unwrap_or("");
529 let sref = builder.alloc_string(value);
530 encode_string_ref_data(sref)
531 }
532 MdastNodeType::Code => {
533 let lang_ref = alloc_opt_str(builder, js_node.lang.as_deref());
534 let meta_ref = alloc_opt_str(builder, js_node.meta.as_deref());
535 let value_ref = alloc_opt_str(builder, js_node.value.as_deref());
536 encode_code_data(lang_ref, meta_ref, value_ref, b'`')
537 }
538 MdastNodeType::Math | MdastNodeType::InlineMath => {
541 let meta_ref = alloc_opt_str(builder, js_node.meta.as_deref());
542 let value_ref = alloc_opt_str(builder, js_node.value.as_deref());
543 encode_math_data(meta_ref, value_ref)
544 }
545 MdastNodeType::Link => {
546 let url_ref = alloc_opt_str(builder, js_node.url.as_deref());
547 let title_ref = alloc_opt_str(builder, js_node.title.as_deref());
548 encode_link_data(url_ref, title_ref)
549 }
550 MdastNodeType::Image => {
551 let url_ref = alloc_opt_str(builder, js_node.url.as_deref());
552 let alt_ref = alloc_opt_str(builder, js_node.alt.as_deref());
553 let title_ref = alloc_opt_str(builder, js_node.title.as_deref());
554 encode_image_data(url_ref, alt_ref, title_ref)
555 }
556 MdastNodeType::Definition => {
557 let url_ref = alloc_opt_str(builder, js_node.url.as_deref());
558 let title_ref = alloc_opt_str(builder, js_node.title.as_deref());
559 let id_ref = alloc_opt_str(builder, js_node.identifier.as_deref());
560 let label_ref = alloc_opt_str(builder, js_node.label.as_deref());
561 encode_definition_data(url_ref, title_ref, id_ref, label_ref)
562 }
563 MdastNodeType::Table => {
564 let alignments = js_table_alignments(js_node.align.as_deref());
565 encode_table_data(&alignments)
566 }
567 MdastNodeType::List => {
568 let ordered = js_node.ordered.unwrap_or(false);
569 let start = js_node.start.unwrap_or(1);
570 let spread = js_node.spread.unwrap_or(false);
571 encode_list_data(ordered, start, spread)
572 }
573 MdastNodeType::ListItem => {
574 let checked = match js_node.checked {
575 Some(true) => 1u8,
576 Some(false) => 0u8,
577 None => 2u8, };
579 let spread = js_node.spread.unwrap_or(false);
580 encode_list_item_data(checked, spread)
581 }
582 MdastNodeType::LinkReference | MdastNodeType::FootnoteReference => {
583 let id_ref = alloc_opt_str(builder, js_node.identifier.as_deref());
584 let label_ref = alloc_opt_str(builder, js_node.label.as_deref());
585 encode_reference_data(id_ref, label_ref, js_reference_kind(js_node))
586 }
587 MdastNodeType::ImageReference => {
589 let id_ref = alloc_opt_str(builder, js_node.identifier.as_deref());
590 let label_ref = alloc_opt_str(builder, js_node.label.as_deref());
591 let alt_ref = alloc_opt_str(builder, js_node.alt.as_deref());
592 encode_image_reference_data(id_ref, label_ref, js_reference_kind(js_node), alt_ref)
593 }
594 MdastNodeType::FootnoteDefinition => {
595 let id_ref = alloc_opt_str(builder, js_node.identifier.as_deref());
596 let label_ref = alloc_opt_str(builder, js_node.label.as_deref());
597 encode_footnote_definition_data(id_ref, label_ref)
598 }
599 #[cfg(feature = "mdx")]
600 MdastNodeType::MdxJsxFlowElement | MdastNodeType::MdxJsxTextElement => {
601 let name_ref = alloc_opt_str(builder, js_node.name.as_deref());
602 let attr_tuples = encode_js_jsx_attrs(
603 builder,
604 js_node.attributes.as_ref().and_then(|a| a.as_jsx()),
605 );
606 let explicit = js_data_is_mdx_explicit(&js_node.data);
607 encode_mdx_jsx_element_data(name_ref, &attr_tuples, explicit)
608 }
609 MdastNodeType::ContainerDirective
610 | MdastNodeType::LeafDirective
611 | MdastNodeType::TextDirective => {
612 let name = js_node.name.as_deref().unwrap_or("");
613 let name_ref = builder.alloc_string(name);
614 let attr_pairs = encode_js_directive_attrs(builder, js_node.attributes.as_ref());
615 encode_directive_data(name_ref, &attr_pairs)
616 }
617 #[cfg(feature = "mdx")]
618 MdastNodeType::MdxFlowExpression
619 | MdastNodeType::MdxTextExpression
620 | MdastNodeType::MdxjsEsm => {
621 let value_ref = alloc_opt_str(builder, js_node.value.as_deref());
622 encode_expression_data(value_ref)
623 }
624 _ => Vec::new(),
626 }
627}
628
629fn js_table_alignments(align: Option<&[Option<String>]>) -> Vec<ColumnAlign> {
630 let Some(align) = align else {
631 return Vec::new();
632 };
633 align
634 .iter()
635 .map(|v| match v.as_deref() {
636 Some("left") => ColumnAlign::Left,
637 Some("right") => ColumnAlign::Right,
638 Some("center") => ColumnAlign::Center,
639 _ => ColumnAlign::None,
640 })
641 .collect()
642}
643
644fn js_reference_kind(js_node: &JsNode) -> u8 {
645 match js_node.reference_type.as_deref() {
646 Some("collapsed") => 1,
647 Some("full") => 2,
648 _ => 0,
649 }
650}
651
652fn encode_js_directive_attrs(
653 builder: &mut ArenaBuilder<Mdast>,
654 attrs: Option<&satteri_ast::commands::JsNodeAttributes>,
655) -> Vec<(StringRef, StringRef)> {
656 let Some(map) = attrs.and_then(|a| a.as_directive()) else {
657 return Vec::new();
658 };
659 map.iter()
660 .filter_map(|(k, v)| {
661 let val = v.as_str()?;
662 Some((builder.alloc_string(k), builder.alloc_string(val)))
663 })
664 .collect()
665}
666
667fn alloc_opt_str<K: ArenaKind>(builder: &mut ArenaBuilder<K>, s: Option<&str>) -> StringRef {
668 match s {
669 Some(v) if !v.is_empty() => builder.alloc_string(v),
670 _ => StringRef::empty(),
671 }
672}
673
674fn name_to_node_type(name: &str) -> Result<MdastNodeType, CommandError> {
675 match name {
676 "root" => Ok(MdastNodeType::Root),
677 "paragraph" => Ok(MdastNodeType::Paragraph),
678 "heading" => Ok(MdastNodeType::Heading),
679 "thematicBreak" => Ok(MdastNodeType::ThematicBreak),
680 "blockquote" => Ok(MdastNodeType::Blockquote),
681 "list" => Ok(MdastNodeType::List),
682 "listItem" => Ok(MdastNodeType::ListItem),
683 "html" => Ok(MdastNodeType::Html),
684 "code" => Ok(MdastNodeType::Code),
685 "definition" => Ok(MdastNodeType::Definition),
686 "text" => Ok(MdastNodeType::Text),
687 "emphasis" => Ok(MdastNodeType::Emphasis),
688 "strong" => Ok(MdastNodeType::Strong),
689 "inlineCode" => Ok(MdastNodeType::InlineCode),
690 "break" => Ok(MdastNodeType::Break),
691 "link" => Ok(MdastNodeType::Link),
692 "image" => Ok(MdastNodeType::Image),
693 "linkReference" => Ok(MdastNodeType::LinkReference),
694 "imageReference" => Ok(MdastNodeType::ImageReference),
695 "footnoteDefinition" => Ok(MdastNodeType::FootnoteDefinition),
696 "footnoteReference" => Ok(MdastNodeType::FootnoteReference),
697 "table" => Ok(MdastNodeType::Table),
698 "tableRow" => Ok(MdastNodeType::TableRow),
699 "tableCell" => Ok(MdastNodeType::TableCell),
700 "delete" => Ok(MdastNodeType::Delete),
701 "yaml" => Ok(MdastNodeType::Yaml),
702 "toml" => Ok(MdastNodeType::Toml),
703 "math" => Ok(MdastNodeType::Math),
704 "inlineMath" => Ok(MdastNodeType::InlineMath),
705 "containerDirective" => Ok(MdastNodeType::ContainerDirective),
706 "leafDirective" => Ok(MdastNodeType::LeafDirective),
707 "textDirective" => Ok(MdastNodeType::TextDirective),
708 #[cfg(feature = "mdx")]
709 "mdxJsxFlowElement" => Ok(MdastNodeType::MdxJsxFlowElement),
710 #[cfg(feature = "mdx")]
711 "mdxJsxTextElement" => Ok(MdastNodeType::MdxJsxTextElement),
712 #[cfg(feature = "mdx")]
713 "mdxFlowExpression" => Ok(MdastNodeType::MdxFlowExpression),
714 #[cfg(feature = "mdx")]
715 "mdxTextExpression" => Ok(MdastNodeType::MdxTextExpression),
716 #[cfg(feature = "mdx")]
717 "mdxjsEsm" => Ok(MdastNodeType::MdxjsEsm),
718 other => Err(CommandError::UnknownNodeType(other.to_string())),
719 }
720}
721
722fn apply_hast_set_property(
728 arena: &mut Arena<Hast>,
729 node_id: u32,
730 prop_name: &str,
731 value_type: u8,
732 value_str: &str,
733) -> Result<(), CommandError> {
734 if prop_name == "data" {
735 apply_data_property(arena, node_id, value_type, value_str);
736 return Ok(());
737 }
738
739 let raw_type = arena.get_node(node_id).node_type;
740 let node_type = HastNodeType::from_u8(raw_type)
741 .ok_or_else(|| CommandError::UnknownNodeType(format!("hast type 0x{raw_type:02x}")))?;
742
743 match node_type {
744 HastNodeType::Element => {
745 apply_hast_element_property(arena, node_id, prop_name, value_type, value_str)
746 }
747
748 HastNodeType::Text
749 | HastNodeType::Comment
750 | HastNodeType::Raw
751 | HastNodeType::MdxFlowExpression
752 | HastNodeType::MdxTextExpression
753 | HastNodeType::MdxEsm
754 if prop_name == "value" =>
755 {
756 let sref = arena.alloc_string(value_str);
757 let data = arena.get_type_data(node_id);
758 if data.len() >= 8 {
759 let data_offset = arena.get_node(node_id).data_offset as usize;
760 arena.type_data[data_offset..data_offset + 4]
761 .copy_from_slice(&sref.offset.to_le_bytes());
762 arena.type_data[data_offset + 4..data_offset + 8]
763 .copy_from_slice(&sref.len.to_le_bytes());
764 Ok(())
765 } else {
766 Err(CommandError::UnexpectedEof)
767 }
768 }
769
770 _ => Err(CommandError::UnknownField {
771 node_type: node_type.name().to_string(),
772 name: prop_name.to_string(),
773 }),
774 }
775}
776
777fn apply_hast_element_property(
779 arena: &mut Arena<Hast>,
780 node_id: u32,
781 prop_name: &str,
782 value_type: u8,
783 value_str: &str,
784) -> Result<(), CommandError> {
785 let old_data = arena.get_type_data(node_id).to_vec();
786 if old_data.len() < 16 {
787 return Err(CommandError::UnexpectedEof);
788 }
789
790 let old_prop_count = u32::from_le_bytes(old_data[8..12].try_into().unwrap()) as usize;
791
792 let mut found_index: Option<usize> = None;
793 for i in 0..old_prop_count {
794 let base = 16 + i * 20;
795 let name_off = u32::from_le_bytes(old_data[base..base + 4].try_into().unwrap());
796 let name_len = u32::from_le_bytes(old_data[base + 4..base + 8].try_into().unwrap());
797 let existing_name = arena.get_str(StringRef::new(name_off, name_len));
798 if existing_name == prop_name {
799 found_index = Some(i);
800 break;
801 }
802 }
803
804 let name_ref = arena.alloc_string(prop_name);
805 let val_ref = if value_str.is_empty() {
806 StringRef::empty()
807 } else {
808 arena.alloc_string(value_str)
809 };
810
811 if let Some(idx) = found_index {
812 let mut new_data = old_data;
813 let base = 16 + idx * 20;
814 new_data[base..base + 4].copy_from_slice(&name_ref.offset.to_le_bytes());
815 new_data[base + 4..base + 8].copy_from_slice(&name_ref.len.to_le_bytes());
816 new_data[base + 8] = value_type;
817 new_data[base + 9..base + 12].copy_from_slice(&[0u8; 3]);
818 new_data[base + 12..base + 16].copy_from_slice(&val_ref.offset.to_le_bytes());
819 new_data[base + 16..base + 20].copy_from_slice(&val_ref.len.to_le_bytes());
820 arena.set_type_data(node_id, &new_data);
821 } else {
822 let new_prop_count = (old_prop_count + 1) as u32;
823 let mut new_data = Vec::with_capacity(16 + new_prop_count as usize * 20);
824 new_data.extend_from_slice(&old_data[0..8]);
825 new_data.extend_from_slice(&new_prop_count.to_le_bytes());
826 new_data.extend_from_slice(&0u32.to_le_bytes());
827 if old_prop_count > 0 {
828 new_data.extend_from_slice(&old_data[16..16 + old_prop_count * 20]);
829 }
830 new_data.extend_from_slice(&name_ref.offset.to_le_bytes());
831 new_data.extend_from_slice(&name_ref.len.to_le_bytes());
832 new_data.push(value_type);
833 new_data.extend_from_slice(&[0u8; 3]);
834 new_data.extend_from_slice(&val_ref.offset.to_le_bytes());
835 new_data.extend_from_slice(&val_ref.len.to_le_bytes());
836 arena.set_type_data(node_id, &new_data);
837 }
838
839 Ok(())
840}
841
842fn emit_hast_js_node(
844 js_node: &JsNode,
845 builder: &mut ArenaBuilder<Hast>,
846) -> Result<(), CommandError> {
847 if let Some(ref_id) = js_node.ref_id {
848 emit_ref_node(ref_id, builder);
849 return Ok(());
850 }
851
852 let raw_type = name_to_hast_type(&js_node.node_type)
853 .ok_or_else(|| CommandError::UnknownNodeType(js_node.node_type.clone()))?;
854 builder.open_node_raw(raw_type as u8);
855
856 let type_data = encode_hast_js_node_data(js_node, raw_type, builder);
857 if !type_data.is_empty() {
858 builder.set_data_current(&type_data);
859 }
860
861 write_js_node_data(js_node, builder)?;
862
863 if let Some(children) = &js_node.children {
864 for child in children {
865 emit_hast_js_node(child, builder)?;
866 }
867 }
868
869 builder.close_node();
870 Ok(())
871}
872
873fn name_to_hast_type(name: &str) -> Option<HastNodeType> {
874 match name {
875 "root" => Some(HastNodeType::Root),
876 "element" => Some(HastNodeType::Element),
877 "text" => Some(HastNodeType::Text),
878 "comment" => Some(HastNodeType::Comment),
879 "doctype" => Some(HastNodeType::Doctype),
880 "raw" => Some(HastNodeType::Raw),
881 #[cfg(feature = "mdx")]
882 "mdxJsxFlowElement" => Some(HastNodeType::MdxJsxElement),
883 #[cfg(feature = "mdx")]
884 "mdxJsxTextElement" => Some(HastNodeType::MdxJsxTextElement),
885 #[cfg(feature = "mdx")]
886 "mdxFlowExpression" => Some(HastNodeType::MdxFlowExpression),
887 #[cfg(feature = "mdx")]
888 "mdxTextExpression" => Some(HastNodeType::MdxTextExpression),
889 #[cfg(feature = "mdx")]
890 "mdxjsEsm" => Some(HastNodeType::MdxEsm),
891 _ => None,
892 }
893}
894
895fn encode_hast_js_node_data(
896 js_node: &JsNode,
897 node_type: HastNodeType,
898 builder: &mut ArenaBuilder<Hast>,
899) -> Vec<u8> {
900 match node_type {
901 HastNodeType::Element => {
902 let tag = js_node.tag_name.as_deref().unwrap_or("div");
903 let tag_ref = builder.alloc_string(tag);
904
905 let mut props: Vec<(StringRef, u8, StringRef)> = Vec::new();
906 if let Some(properties) = &js_node.properties {
907 for (key, value) in properties {
908 let name_ref = builder.alloc_string(key);
909 match value {
910 serde_json::Value::Bool(true) => {
911 props.push((name_ref, PROP_BOOL_TRUE, StringRef::empty()));
912 }
913 serde_json::Value::Bool(false) => {
914 props.push((name_ref, PROP_BOOL_FALSE, StringRef::empty()));
915 }
916 serde_json::Value::String(s) => {
917 let val_ref = builder.alloc_string(s);
918 props.push((name_ref, PROP_STRING, val_ref));
919 }
920 serde_json::Value::Number(n) => {
921 let val_ref = builder.alloc_string(&n.to_string());
922 props.push((name_ref, PROP_INT, val_ref));
923 }
924 serde_json::Value::Array(arr) => {
925 let joined: String = arr
926 .iter()
927 .filter_map(|v| v.as_str())
928 .collect::<Vec<_>>()
929 .join(" ");
930 let val_ref = builder.alloc_string(&joined);
931 props.push((name_ref, PROP_SPACE_SEP, val_ref));
932 }
933 _ => {}
934 }
935 }
936 }
937
938 let mut out = Vec::with_capacity(16 + props.len() * 20);
939 out.extend_from_slice(&tag_ref.offset.to_le_bytes());
940 out.extend_from_slice(&tag_ref.len.to_le_bytes());
941 out.extend_from_slice(&(props.len() as u32).to_le_bytes());
942 out.extend_from_slice(&0u32.to_le_bytes());
943 for (name_ref, kind, val_ref) in &props {
944 out.extend_from_slice(&name_ref.offset.to_le_bytes());
945 out.extend_from_slice(&name_ref.len.to_le_bytes());
946 out.push(*kind);
947 out.extend_from_slice(&[0u8; 3]);
948 out.extend_from_slice(&val_ref.offset.to_le_bytes());
949 out.extend_from_slice(&val_ref.len.to_le_bytes());
950 }
951 out
952 }
953
954 HastNodeType::Text | HastNodeType::Comment | HastNodeType::Raw => {
955 let value = js_node.value.as_deref().unwrap_or("");
956 let sref = builder.alloc_string(value);
957 let mut out = [0u8; 8];
958 out[0..4].copy_from_slice(&sref.offset.to_le_bytes());
959 out[4..8].copy_from_slice(&sref.len.to_le_bytes());
960 out.to_vec()
961 }
962
963 #[cfg(feature = "mdx")]
964 HastNodeType::MdxJsxElement | HastNodeType::MdxJsxTextElement => {
965 let name = js_node
966 .name
967 .as_deref()
968 .or(js_node.tag_name.as_deref())
969 .unwrap_or("");
970 let name_ref = builder.alloc_string(name);
971 let attr_tuples = encode_js_jsx_attrs(
972 builder,
973 js_node.attributes.as_ref().and_then(|a| a.as_jsx()),
974 );
975 let explicit = js_data_is_mdx_explicit(&js_node.data);
976 encode_mdx_jsx_element_data(name_ref, &attr_tuples, explicit)
977 }
978
979 #[cfg(feature = "mdx")]
980 HastNodeType::MdxFlowExpression
981 | HastNodeType::MdxTextExpression
982 | HastNodeType::MdxEsm => {
983 let value = js_node.value.as_deref().unwrap_or("");
984 let sref = builder.alloc_string(value);
985 let mut out = [0u8; 8];
986 out[0..4].copy_from_slice(&sref.offset.to_le_bytes());
987 out[4..8].copy_from_slice(&sref.len.to_le_bytes());
988 out.to_vec()
989 }
990
991 _ => Vec::new(),
992 }
993}
994
995fn read_mdast_payload(
997 reader: &mut BufReader<'_>,
998 parse_markdown: &dyn Fn(&str) -> Arena<Mdast>,
999) -> Result<(Arena<Mdast>, bool), CommandError> {
1000 let payload_type = reader.read_u8()?;
1001 let len = reader.read_u32()? as usize;
1002
1003 match payload_type {
1004 PAYLOAD_RAW_MARKDOWN => {
1005 let md = reader.read_str(len)?;
1006 Ok((parse_raw_markdown(md, parse_markdown), false))
1007 }
1008 PAYLOAD_RAW_HTML => {
1009 let html = reader.read_str(len)?;
1010 let escaped = escape_braces_in_html_text(html);
1011 Ok((parse_raw_markdown(&escaped, parse_markdown), false))
1012 }
1013 PAYLOAD_SERDE_JSON => {
1014 let json_str = reader.read_str(len)?;
1015 let js_node: JsNode = serde_json::from_str(json_str)
1016 .map_err(|e| CommandError::InvalidJson(e.to_string()))?;
1017 js_node_to_mdast_arena(&js_node)
1018 }
1019 other => Err(CommandError::UnknownPayloadType(other)),
1020 }
1021}
1022
1023fn read_hast_payload(reader: &mut BufReader<'_>) -> Result<(Arena<Hast>, bool), CommandError> {
1027 let payload_type = reader.read_u8()?;
1028 let len = reader.read_u32()? as usize;
1029
1030 match payload_type {
1031 PAYLOAD_SERDE_JSON => {
1032 let json_str = reader.read_str(len)?;
1033 let js_node: JsNode = serde_json::from_str(json_str)
1034 .map_err(|e| CommandError::InvalidJson(e.to_string()))?;
1035 js_node_to_hast_arena(&js_node)
1036 }
1037 other => Err(CommandError::UnknownPayloadType(other)),
1038 }
1039}
1040
1041pub fn apply_mdast_commands(
1064 arena: Arena<Mdast>,
1065 command_buf: &[u8],
1066 parse_markdown: &dyn Fn(&str) -> Arena<Mdast>,
1067) -> Result<Arena<Mdast>, CommandError> {
1068 let (arena, dropped) = apply_mdast_commands_lenient(arena, command_buf, parse_markdown)?;
1069 if let Some(anchor) = dropped.first() {
1070 return Err(CommandError::PatchOnRemovedSubtree(*anchor));
1071 }
1072 Ok(arena)
1073}
1074
1075pub fn apply_mdast_commands_lenient(
1082 mut arena: Arena<Mdast>,
1083 command_buf: &[u8],
1084 parse_markdown: &dyn Fn(&str) -> Arena<Mdast>,
1085) -> Result<(Arena<Mdast>, Vec<u32>), CommandError> {
1086 if command_buf.is_empty() {
1087 return Ok((arena, Vec::new()));
1088 }
1089
1090 let mut patches: Vec<Patch<Mdast>> = Vec::new();
1091 let mut reader = BufReader::new(command_buf);
1092
1093 while reader.remaining() > 0 {
1094 let cmd = reader.read_u8()?;
1095
1096 match cmd {
1097 CMD_REMOVE => {
1098 let node_id = reader.read_u32()?;
1099 patches.push(Patch::Remove { node_id });
1100 }
1101
1102 CMD_SET_PROPERTY => {
1103 let node_id = reader.read_u32()?;
1104 let value_type = reader.read_u8()?;
1105 let name_len = reader.read_u32()? as usize;
1106 let name = reader.read_str(name_len)?;
1107 let value_len = reader.read_u32()? as usize;
1108 let value = reader.read_str(value_len)?;
1109 apply_mdast_set_property(&mut arena, node_id, name, value_type, value)?;
1110 }
1111
1112 CMD_INSERT_BEFORE => {
1113 let node_id = reader.read_u32()?;
1114 let (new_tree, _) = read_mdast_payload(&mut reader, parse_markdown)?;
1115 patches.push(Patch::InsertBefore { node_id, new_tree });
1116 }
1117
1118 CMD_INSERT_AFTER => {
1119 let node_id = reader.read_u32()?;
1120 let (new_tree, _) = read_mdast_payload(&mut reader, parse_markdown)?;
1121 patches.push(Patch::InsertAfter { node_id, new_tree });
1122 }
1123
1124 CMD_PREPEND_CHILD => {
1125 let node_id = reader.read_u32()?;
1126 let (child_tree, _) = read_mdast_payload(&mut reader, parse_markdown)?;
1127 patches.push(Patch::PrependChild {
1128 node_id,
1129 child_tree,
1130 });
1131 }
1132
1133 CMD_APPEND_CHILD => {
1134 let node_id = reader.read_u32()?;
1135 let (child_tree, _) = read_mdast_payload(&mut reader, parse_markdown)?;
1136 patches.push(Patch::AppendChild {
1137 node_id,
1138 child_tree,
1139 });
1140 }
1141
1142 CMD_WRAP => {
1143 let node_id = reader.read_u32()?;
1144 let (parent_tree, _) = read_mdast_payload(&mut reader, parse_markdown)?;
1145 patches.push(Patch::Wrap {
1146 node_id,
1147 parent_tree,
1148 });
1149 }
1150
1151 CMD_REPLACE => {
1152 let node_id = reader.read_u32()?;
1153 let (new_tree, keep_children) = read_mdast_payload(&mut reader, parse_markdown)?;
1154 patches.push(Patch::Replace {
1155 node_id,
1156 new_tree,
1157 keep_children,
1158 });
1159 }
1160
1161 CMD_SET_CHILDREN => {
1162 let node_id = reader.read_u32()?;
1163 let (new_children, _) = read_mdast_payload(&mut reader, parse_markdown)?;
1164 patches.push(Patch::SetChildren {
1165 node_id,
1166 new_children,
1167 });
1168 }
1169
1170 other => return Err(CommandError::UnknownCommand(other)),
1171 }
1172 }
1173
1174 if patches.is_empty() {
1175 Ok((arena, Vec::new()))
1176 } else {
1177 let result = satteri_ast::rebuild::rebuild_lenient(&arena, &patches)?;
1178 Ok((result.arena, result.dropped))
1179 }
1180}
1181
1182pub fn apply_hast_commands(
1200 arena: Arena<Hast>,
1201 command_buf: &[u8],
1202) -> Result<Arena<Hast>, CommandError> {
1203 let (arena, dropped) = apply_hast_commands_lenient(arena, command_buf)?;
1204 if let Some(anchor) = dropped.first() {
1205 return Err(CommandError::PatchOnRemovedSubtree(*anchor));
1206 }
1207 Ok(arena)
1208}
1209
1210pub fn apply_hast_commands_lenient(
1216 mut arena: Arena<Hast>,
1217 command_buf: &[u8],
1218) -> Result<(Arena<Hast>, Vec<u32>), CommandError> {
1219 if command_buf.is_empty() {
1220 return Ok((arena, Vec::new()));
1221 }
1222
1223 let mut patches: Vec<Patch<Hast>> = Vec::new();
1224 let mut reader = BufReader::new(command_buf);
1225
1226 while reader.remaining() > 0 {
1227 let cmd = reader.read_u8()?;
1228
1229 match cmd {
1230 CMD_REMOVE => {
1231 let node_id = reader.read_u32()?;
1232 patches.push(Patch::Remove { node_id });
1233 }
1234
1235 CMD_SET_PROPERTY => {
1236 let node_id = reader.read_u32()?;
1237 let value_type = reader.read_u8()?;
1238 let name_len = reader.read_u32()? as usize;
1239 let name = reader.read_str(name_len)?;
1240 let value_len = reader.read_u32()? as usize;
1241 let value = reader.read_str(value_len)?;
1242 apply_hast_set_property(&mut arena, node_id, name, value_type, value)?;
1243 }
1244
1245 CMD_INSERT_BEFORE => {
1246 let node_id = reader.read_u32()?;
1247 let (new_tree, _) = read_hast_payload(&mut reader)?;
1248 patches.push(Patch::InsertBefore { node_id, new_tree });
1249 }
1250
1251 CMD_INSERT_AFTER => {
1252 let node_id = reader.read_u32()?;
1253 let (new_tree, _) = read_hast_payload(&mut reader)?;
1254 patches.push(Patch::InsertAfter { node_id, new_tree });
1255 }
1256
1257 CMD_PREPEND_CHILD => {
1258 let node_id = reader.read_u32()?;
1259 let (child_tree, _) = read_hast_payload(&mut reader)?;
1260 patches.push(Patch::PrependChild {
1261 node_id,
1262 child_tree,
1263 });
1264 }
1265
1266 CMD_APPEND_CHILD => {
1267 let node_id = reader.read_u32()?;
1268 let (child_tree, _) = read_hast_payload(&mut reader)?;
1269 patches.push(Patch::AppendChild {
1270 node_id,
1271 child_tree,
1272 });
1273 }
1274
1275 CMD_WRAP => {
1276 let node_id = reader.read_u32()?;
1277 let (parent_tree, _) = read_hast_payload(&mut reader)?;
1278 patches.push(Patch::Wrap {
1279 node_id,
1280 parent_tree,
1281 });
1282 }
1283
1284 CMD_REPLACE => {
1285 let node_id = reader.read_u32()?;
1286 let (new_tree, keep_children) = read_hast_payload(&mut reader)?;
1287 patches.push(Patch::Replace {
1288 node_id,
1289 new_tree,
1290 keep_children,
1291 });
1292 }
1293
1294 CMD_SET_CHILDREN => {
1295 let node_id = reader.read_u32()?;
1296 let (new_children, _) = read_hast_payload(&mut reader)?;
1297 patches.push(Patch::SetChildren {
1298 node_id,
1299 new_children,
1300 });
1301 }
1302
1303 other => return Err(CommandError::UnknownCommand(other)),
1304 }
1305 }
1306
1307 if patches.is_empty() {
1308 Ok((arena, Vec::new()))
1309 } else {
1310 let result = satteri_ast::rebuild::rebuild_lenient(&arena, &patches)?;
1311 Ok((result.arena, result.dropped))
1312 }
1313}
1314
1315#[cfg(test)]
1316mod tests {
1317 use super::*;
1318 use satteri_ast::shared::PROP_INT;
1319
1320 fn test_parse_markdown(source: &str) -> Arena<Mdast> {
1321 let mut b = ArenaBuilder::<Mdast>::new(String::new());
1322 b.open_node(MdastNodeType::Root as u8);
1323 b.open_node(MdastNodeType::Paragraph as u8);
1324 b.open_node(MdastNodeType::Text as u8);
1325 let sref = b.alloc_string(source);
1326 b.set_data_current(&satteri_arena::encode_string_ref_data(sref));
1327 b.close_node();
1328 b.close_node();
1329 b.close_node();
1330 b.finish()
1331 }
1332
1333 fn push_u32(buf: &mut Vec<u8>, v: u32) {
1334 buf.extend_from_slice(&v.to_le_bytes());
1335 }
1336
1337 fn push_set_property(buf: &mut Vec<u8>, node_id: u32, value_type: u8, name: &str, value: &str) {
1339 buf.push(CMD_SET_PROPERTY);
1340 push_u32(buf, node_id);
1341 buf.push(value_type);
1342 push_u32(buf, name.len() as u32);
1343 buf.extend_from_slice(name.as_bytes());
1344 push_u32(buf, value.len() as u32);
1345 buf.extend_from_slice(value.as_bytes());
1346 }
1347
1348 fn build_hello_world() -> Arena<Mdast> {
1349 use satteri_ast::mdast::codec::{encode_heading_data, encode_string_ref_data};
1350
1351 let source = "# Hello\n\nWorld".to_string();
1352 let mut b = ArenaBuilder::<Mdast>::new(source);
1353
1354 b.open_node(MdastNodeType::Root as u8);
1355 b.set_position_current(0, 14, 1, 1, 2, 6);
1356
1357 b.open_node(MdastNodeType::Heading as u8);
1358 b.set_position_current(0, 7, 1, 1, 1, 8);
1359 b.set_data_current(&encode_heading_data(1));
1360
1361 b.open_node(MdastNodeType::Text as u8);
1362 b.set_position_current(2, 7, 1, 3, 1, 8);
1363 b.set_data_current(&encode_string_ref_data(StringRef::new(2, 5)));
1364 b.close_node();
1365
1366 b.close_node();
1367
1368 b.open_node(MdastNodeType::Paragraph as u8);
1369 b.set_position_current(9, 14, 2, 1, 2, 6);
1370
1371 b.open_node(MdastNodeType::Text as u8);
1372 b.set_position_current(9, 14, 2, 1, 2, 6);
1373 b.set_data_current(&encode_string_ref_data(StringRef::new(9, 5)));
1374 b.close_node();
1375
1376 b.close_node();
1377 b.close_node();
1378
1379 b.finish()
1380 }
1381
1382 #[test]
1383 fn empty_command_buffer() {
1384 let arena = build_hello_world();
1385 let result = apply_mdast_commands(arena.clone(), &[], &test_parse_markdown).unwrap();
1386 assert_eq!(result.len(), arena.len());
1387 }
1388
1389 #[test]
1390 fn remove_command() {
1391 let arena = build_hello_world();
1392 let heading_id = arena.get_children(0)[0];
1393 let mut buf = Vec::new();
1394 buf.push(CMD_REMOVE);
1395 push_u32(&mut buf, heading_id);
1396
1397 let result = apply_mdast_commands(arena.clone(), &buf, &test_parse_markdown).unwrap();
1398 assert_eq!(result.get_children(0).len(), 1);
1399 assert_eq!(
1400 result.get_node(result.get_children(0)[0]).node_type,
1401 MdastNodeType::Paragraph as u8
1402 );
1403 }
1404
1405 #[test]
1406 fn set_property_heading_depth() {
1407 let arena = build_hello_world();
1408 let heading_id = arena.get_children(0)[0];
1409
1410 let mut buf = Vec::new();
1411 push_set_property(&mut buf, heading_id, PROP_INT, "depth", "3");
1412
1413 let result = apply_mdast_commands(arena.clone(), &buf, &test_parse_markdown).unwrap();
1414 let heading_data = result.get_type_data(heading_id);
1415 let heading = decode_heading_data(heading_data);
1416 assert_eq!(heading.depth, 3);
1417 }
1418
1419 #[test]
1420 fn set_property_text_value() {
1421 let arena = build_hello_world();
1422 let heading_id = arena.get_children(0)[0];
1423 let text_id = arena.get_children(heading_id)[0];
1424
1425 let mut buf = Vec::new();
1426 push_set_property(&mut buf, text_id, PROP_STRING, "value", "Goodbye");
1427
1428 let result = apply_mdast_commands(arena.clone(), &buf, &test_parse_markdown).unwrap();
1429 let text_data = result.get_type_data(text_id);
1430 let sref = decode_string_ref_data(text_data);
1431 assert_eq!(result.get_str(sref), "Goodbye");
1432 }
1433
1434 #[test]
1435 fn replace_with_raw_markdown() {
1436 let arena = build_hello_world();
1437 let heading_id = arena.get_children(0)[0];
1438
1439 let raw_md = "## New Heading";
1440 let mut buf = Vec::new();
1441 buf.push(CMD_REPLACE);
1442 push_u32(&mut buf, heading_id);
1443 buf.push(PAYLOAD_RAW_MARKDOWN);
1444 push_u32(&mut buf, raw_md.len() as u32);
1445 buf.extend_from_slice(raw_md.as_bytes());
1446
1447 let result = apply_mdast_commands(arena.clone(), &buf, &test_parse_markdown).unwrap();
1448 let root_children = result.get_children(0);
1449 assert!(root_children.len() >= 2);
1450 }
1451
1452 #[test]
1453 fn replace_with_serde_json() {
1454 let arena = build_hello_world();
1455 let heading_id = arena.get_children(0)[0];
1456
1457 let json =
1458 r#"{"type":"heading","depth":2,"children":[{"type":"text","value":"Replaced"}]}"#;
1459 let mut buf = Vec::new();
1460 buf.push(CMD_REPLACE);
1461 push_u32(&mut buf, heading_id);
1462 buf.push(PAYLOAD_SERDE_JSON);
1463 push_u32(&mut buf, json.len() as u32);
1464 buf.extend_from_slice(json.as_bytes());
1465
1466 let result = apply_mdast_commands(arena.clone(), &buf, &test_parse_markdown).unwrap();
1467 let root_children = result.get_children(0);
1468 assert_eq!(root_children.len(), 2);
1469 let new_heading = root_children[0];
1470 assert_eq!(
1471 result.get_node(new_heading).node_type,
1472 MdastNodeType::Heading as u8
1473 );
1474 let heading_data = result.get_type_data(new_heading);
1475 assert_eq!(decode_heading_data(heading_data).depth, 2);
1476 }
1477
1478 #[test]
1479 fn replace_with_directive_child() {
1480 let arena = build_hello_world();
1485 let heading_id = arena.get_children(0)[0];
1486
1487 let json = r#"{"type":"paragraph","children":[{"type":"text","value":"hi "},{"type":"textDirective","name":"inline","attributes":{},"children":[]}]}"#;
1488 let mut buf = Vec::new();
1489 buf.push(CMD_REPLACE);
1490 push_u32(&mut buf, heading_id);
1491 buf.push(PAYLOAD_SERDE_JSON);
1492 push_u32(&mut buf, json.len() as u32);
1493 buf.extend_from_slice(json.as_bytes());
1494
1495 let result = apply_mdast_commands(arena.clone(), &buf, &test_parse_markdown).unwrap();
1496 let root_children = result.get_children(0);
1497 let new_para = root_children[0];
1498 assert_eq!(
1499 result.get_node(new_para).node_type,
1500 MdastNodeType::Paragraph as u8
1501 );
1502 let para_children = result.get_children(new_para);
1503 assert_eq!(para_children.len(), 2);
1504 let directive = para_children[1];
1505 assert_eq!(
1506 result.get_node(directive).node_type,
1507 MdastNodeType::TextDirective as u8
1508 );
1509 let dir_data = result.get_type_data(directive);
1510 assert_eq!(decode_directive_attr_count(dir_data), 0);
1511 }
1512
1513 #[test]
1514 fn replace_with_directive_attrs() {
1515 let arena = build_hello_world();
1518 let heading_id = arena.get_children(0)[0];
1519
1520 let json = r#"{"type":"containerDirective","name":"tip","attributes":{"id":"foo","class":"bar"},"children":[]}"#;
1521 let mut buf = Vec::new();
1522 buf.push(CMD_REPLACE);
1523 push_u32(&mut buf, heading_id);
1524 buf.push(PAYLOAD_SERDE_JSON);
1525 push_u32(&mut buf, json.len() as u32);
1526 buf.extend_from_slice(json.as_bytes());
1527
1528 let result = apply_mdast_commands(arena.clone(), &buf, &test_parse_markdown).unwrap();
1529 let directive = result.get_children(0)[0];
1530 assert_eq!(
1531 result.get_node(directive).node_type,
1532 MdastNodeType::ContainerDirective as u8
1533 );
1534 let dir_data = result.get_type_data(directive);
1535 assert_eq!(decode_directive_attr_count(dir_data), 2);
1536 }
1537
1538 #[test]
1539 fn multiple_commands() {
1540 let arena = build_hello_world();
1541 let heading_id = arena.get_children(0)[0];
1542 let text_id = arena.get_children(heading_id)[0];
1543
1544 let mut buf = Vec::new();
1545 push_set_property(&mut buf, heading_id, PROP_INT, "depth", "3");
1546 push_set_property(&mut buf, text_id, PROP_STRING, "value", "Hi");
1547
1548 let result = apply_mdast_commands(arena.clone(), &buf, &test_parse_markdown).unwrap();
1549
1550 let heading_data = result.get_type_data(heading_id);
1551 assert_eq!(decode_heading_data(heading_data).depth, 3);
1552
1553 let text_data = result.get_type_data(text_id);
1554 let sref = decode_string_ref_data(text_data);
1555 assert_eq!(result.get_str(sref), "Hi");
1556 }
1557
1558 #[test]
1559 fn set_property_null() {
1560 let arena = build_hello_world();
1561 let heading_id = arena.get_children(0)[0];
1562 let text_id = arena.get_children(heading_id)[0];
1563
1564 let mut buf = Vec::new();
1565 push_set_property(&mut buf, text_id, PROP_NULL, "value", "");
1566
1567 let result = apply_mdast_commands(arena.clone(), &buf, &test_parse_markdown).unwrap();
1568 let text_data = result.get_type_data(text_id);
1569 let sref = decode_string_ref_data(text_data);
1570 assert_eq!(sref.len, 0);
1571 }
1572
1573 #[test]
1574 fn set_property_invalid_field_reports_property_and_node_type() {
1575 let arena = build_hello_world();
1576 let heading_id = arena.get_children(0)[0];
1577
1578 let mut buf = Vec::new();
1579 push_set_property(&mut buf, heading_id, PROP_STRING, "value", "x");
1580
1581 let err = apply_mdast_commands(arena, &buf, &test_parse_markdown).unwrap_err();
1582 assert!(matches!(
1583 err,
1584 CommandError::UnknownField { ref name, ref node_type }
1585 if name == "value" && node_type == "heading"
1586 ));
1587 assert_eq!(
1588 err.to_string(),
1589 "cannot set property 'value' on a 'heading' node"
1590 );
1591 }
1592
1593 #[test]
1594 fn set_property_wrong_value_type_reports_value_mismatch() {
1595 let arena = build_hello_world();
1596 let heading_id = arena.get_children(0)[0];
1597
1598 let mut buf = Vec::new();
1600 push_set_property(&mut buf, heading_id, PROP_STRING, "depth", "3");
1601
1602 let err = apply_mdast_commands(arena, &buf, &test_parse_markdown).unwrap_err();
1603 assert!(matches!(
1604 err,
1605 CommandError::InvalidPropertyValue { ref name, ref node_type }
1606 if name == "depth" && node_type == "heading"
1607 ));
1608 assert_eq!(
1609 err.to_string(),
1610 "property 'depth' on a 'heading' node cannot hold a value of this type"
1611 );
1612 }
1613
1614 #[test]
1615 fn js_node_to_arena_basic() {
1616 let js = JsNode {
1617 node_type: "heading".to_string(),
1618 children: Some(vec![JsNode {
1619 node_type: "text".to_string(),
1620 children: None,
1621 value: Some("Hello".to_string()),
1622 depth: None,
1623 url: None,
1624 title: None,
1625 alt: None,
1626 lang: None,
1627 meta: None,
1628 ordered: None,
1629 start: None,
1630 spread: None,
1631 checked: None,
1632 align: None,
1633 identifier: None,
1634 label: None,
1635 reference_type: None,
1636 name: None,
1637 attributes: None,
1638 tag_name: None,
1639 properties: None,
1640 is_hast: false,
1641 keep_children: false,
1642 ref_id: None,
1643 data: None,
1644 }]),
1645 depth: Some(2),
1646 value: None,
1647 url: None,
1648 title: None,
1649 alt: None,
1650 lang: None,
1651 meta: None,
1652 ordered: None,
1653 start: None,
1654 spread: None,
1655 checked: None,
1656 align: None,
1657 identifier: None,
1658 label: None,
1659 reference_type: None,
1660 name: None,
1661 attributes: None,
1662 tag_name: None,
1663 properties: None,
1664 is_hast: false,
1665 keep_children: false,
1666 ref_id: None,
1667 data: None,
1668 };
1669
1670 let (arena, _keep) = js_node_to_mdast_arena(&js).unwrap();
1671 assert_eq!(arena.len(), 2);
1672 assert_eq!(arena.get_node(0).node_type, MdastNodeType::Heading as u8);
1673 assert_eq!(arena.get_children(0).len(), 1);
1674 let text_id = arena.get_children(0)[0];
1675 assert_eq!(arena.get_node(text_id).node_type, MdastNodeType::Text as u8);
1676 }
1677
1678 #[test]
1679 fn escape_braces_in_html_text_basic() {
1680 assert_eq!(
1681 escape_braces_in_html_text("<span>{foo: 1}</span>"),
1682 "<span>{'{'}foo: 1{'}'}</span>"
1683 );
1684 }
1685
1686 #[test]
1687 fn escape_braces_preserves_attributes() {
1688 let result = escape_braces_in_html_text(r#"<span data-x="{a}">{b}</span>"#);
1689 assert!(
1690 result.contains(r#"data-x="{a}""#),
1691 "attribute braces preserved"
1692 );
1693 assert!(result.contains("{'{'}"), "text braces escaped");
1694 }
1695
1696 #[test]
1697 fn escape_braces_no_braces() {
1698 let html = r#"<pre class="shiki"><code><span style="color:red">hello</span></code></pre>"#;
1699 assert_eq!(escape_braces_in_html_text(html), html);
1700 }
1701
1702 #[test]
1703 fn escape_braces_shiki_output() {
1704 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>"#;
1705 let escaped = escape_braces_in_html_text(html);
1706 assert!(
1707 !escaped.contains(">{<"),
1708 "bare braces in text should be escaped"
1709 );
1710 assert!(
1711 !escaped.contains(">}<"),
1712 "bare braces in text should be escaped"
1713 );
1714 assert!(escaped.contains(r#"class="shiki""#));
1715 assert!(escaped.contains(r#"style="color:#E1E4E8""#));
1716 }
1717
1718 #[test]
1719 fn hast_set_property_add_new() {
1720 let arena = build_hast_element(&[]);
1721 let element_id = arena.get_children(0)[0];
1722
1723 let mut buf = Vec::new();
1724 push_set_property(&mut buf, element_id, PROP_STRING, "class", "test");
1725
1726 let result = apply_hast_commands(arena.clone(), &buf).unwrap();
1727 let data = result.get_type_data(element_id);
1728 let prop_count = u32::from_le_bytes(data[8..12].try_into().unwrap());
1729 assert_eq!(prop_count, 1);
1730 let name_ref = StringRef::new(
1731 u32::from_le_bytes(data[16..20].try_into().unwrap()),
1732 u32::from_le_bytes(data[20..24].try_into().unwrap()),
1733 );
1734 assert_eq!(result.get_str(name_ref), "class");
1735 let val_ref = StringRef::new(
1736 u32::from_le_bytes(data[28..32].try_into().unwrap()),
1737 u32::from_le_bytes(data[32..36].try_into().unwrap()),
1738 );
1739 assert_eq!(result.get_str(val_ref), "test");
1740 assert_eq!(data[24], PROP_STRING);
1741 }
1742
1743 #[test]
1744 fn hast_set_property_overwrite_existing() {
1745 let arena = build_hast_element(&[("class", PROP_STRING, "old")]);
1746 let element_id = arena.get_children(0)[0];
1747
1748 let mut buf = Vec::new();
1749 push_set_property(&mut buf, element_id, PROP_STRING, "class", "new-value");
1750
1751 let result = apply_hast_commands(arena.clone(), &buf).unwrap();
1752 let data = result.get_type_data(element_id);
1753 let prop_count = u32::from_le_bytes(data[8..12].try_into().unwrap());
1754 assert_eq!(prop_count, 1);
1755 let val_ref = StringRef::new(
1756 u32::from_le_bytes(data[28..32].try_into().unwrap()),
1757 u32::from_le_bytes(data[32..36].try_into().unwrap()),
1758 );
1759 assert_eq!(result.get_str(val_ref), "new-value");
1760 }
1761
1762 #[test]
1763 fn hast_set_property_bool_true() {
1764 let arena = build_hast_element(&[]);
1765 let element_id = arena.get_children(0)[0];
1766
1767 let mut buf = Vec::new();
1768 push_set_property(&mut buf, element_id, PROP_BOOL_TRUE, "disabled", "");
1769
1770 let result = apply_hast_commands(arena.clone(), &buf).unwrap();
1771 let data = result.get_type_data(element_id);
1772 let prop_count = u32::from_le_bytes(data[8..12].try_into().unwrap());
1773 assert_eq!(prop_count, 1);
1774 assert_eq!(data[24], PROP_BOOL_TRUE);
1775 }
1776
1777 #[test]
1778 fn hast_set_property_multiple_on_same_node() {
1779 let arena = build_hast_element(&[]);
1780 let element_id = arena.get_children(0)[0];
1781
1782 let mut buf = Vec::new();
1783 push_set_property(&mut buf, element_id, PROP_STRING, "class", "foo");
1784 push_set_property(&mut buf, element_id, PROP_STRING, "id", "bar");
1785
1786 let result = apply_hast_commands(arena.clone(), &buf).unwrap();
1787 let data = result.get_type_data(element_id);
1788 let prop_count = u32::from_le_bytes(data[8..12].try_into().unwrap());
1789 assert_eq!(prop_count, 2);
1790 }
1791
1792 fn build_hast_element(props: &[(&str, u8, &str)]) -> Arena<Hast> {
1794 use satteri_ast::hast::node::HastNodeType;
1795
1796 let mut b = ArenaBuilder::<Hast>::new(String::new());
1797 b.open_node_raw(HastNodeType::Root as u8);
1798 b.open_node_raw(HastNodeType::Element as u8);
1799 let tag_ref = b.alloc_string("div");
1800 let prop_tuples: Vec<(StringRef, u8, StringRef)> = props
1801 .iter()
1802 .map(|(name, kind, value)| {
1803 let n = b.alloc_string(name);
1804 let v = if value.is_empty() {
1805 StringRef::empty()
1806 } else {
1807 b.alloc_string(value)
1808 };
1809 (n, *kind, v)
1810 })
1811 .collect();
1812 let mut type_data = Vec::with_capacity(16 + prop_tuples.len() * 20);
1813 type_data.extend_from_slice(&tag_ref.offset.to_le_bytes());
1814 type_data.extend_from_slice(&tag_ref.len.to_le_bytes());
1815 type_data.extend_from_slice(&(prop_tuples.len() as u32).to_le_bytes());
1816 type_data.extend_from_slice(&0u32.to_le_bytes());
1817 for (n, kind, v) in &prop_tuples {
1818 type_data.extend_from_slice(&n.offset.to_le_bytes());
1819 type_data.extend_from_slice(&n.len.to_le_bytes());
1820 type_data.push(*kind);
1821 type_data.extend_from_slice(&[0u8; 3]);
1822 type_data.extend_from_slice(&v.offset.to_le_bytes());
1823 type_data.extend_from_slice(&v.len.to_le_bytes());
1824 }
1825 b.set_data_current(&type_data);
1826 b.close_node();
1827 b.close_node();
1828 b.finish()
1829 }
1830}