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