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