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