1use anyhow::{bail, Result};
2use pulldown_cmark::{Event, Options, Parser, Tag, TagEnd};
3use std::collections::HashMap;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct Component {
11 pub name: String,
12 pub attrs: HashMap<String, String>,
14 pub open_start: usize,
16 pub open_end: usize,
18 pub close_start: usize,
20 pub close_end: usize,
22}
23
24impl Component {
25 #[allow(dead_code)] pub fn content<'a>(&self, doc: &'a str) -> &'a str {
28 &doc[self.open_end..self.close_start]
29 }
30
31 pub fn patch_mode(&self) -> Option<&str> {
35 self.attrs.get("patch").map(|s| s.as_str())
36 .or_else(|| self.attrs.get("mode").map(|s| s.as_str()))
37 }
38
39 pub fn replace_content(&self, doc: &str, new_content: &str) -> String {
42 let mut result = String::with_capacity(doc.len() + new_content.len());
43 result.push_str(&doc[..self.open_end]);
44 result.push_str(new_content);
45 result.push_str(&doc[self.close_start..]);
46 result
47 }
48
49 pub fn append_with_caret(&self, doc: &str, content: &str, caret_offset: Option<usize>) -> String {
56 let existing = &doc[self.open_end..self.close_start];
57
58 if let Some(caret) = caret_offset {
59 if caret > self.open_end && caret <= self.close_start {
61 let insert_at = doc[..caret].rfind('\n')
63 .map(|i| i + 1)
64 .unwrap_or(self.open_end);
65
66 let insert_at = insert_at.max(self.open_end);
68
69 let mut result = String::with_capacity(doc.len() + content.len() + 1);
70 result.push_str(&doc[..insert_at]);
71 result.push_str(content.trim_end());
72 result.push('\n');
73 result.push_str(&doc[insert_at..]);
74 return result;
75 }
76 }
77
78 let mut result = String::with_capacity(doc.len() + content.len() + 1);
80 result.push_str(&doc[..self.open_end]);
81 result.push_str(existing.trim_end());
82 result.push('\n');
83 result.push_str(content.trim_end());
84 result.push('\n');
85 result.push_str(&doc[self.close_start..]);
86 result
87 }
88
89 pub fn append_with_boundary(&self, doc: &str, content: &str, boundary_id: &str) -> String {
95 let boundary_marker = format!("<!-- agent:boundary:{} -->", boundary_id);
96 let content_region = &doc[self.open_end..self.close_start];
97
98 if let Some(rel_pos) = content_region.find(&boundary_marker) {
99 let abs_pos = self.open_end + rel_pos;
100
101 let line_start = doc[..abs_pos]
103 .rfind('\n')
104 .map(|i| i + 1)
105 .unwrap_or(self.open_end)
106 .max(self.open_end);
107
108 let marker_end = abs_pos + boundary_marker.len();
110 let line_end = if marker_end < self.close_start
111 && doc.as_bytes().get(marker_end) == Some(&b'\n')
112 {
113 marker_end + 1
114 } else {
115 marker_end
116 };
117 let line_end = line_end.min(self.close_start);
118
119 let mut result = String::with_capacity(doc.len() + content.len());
121 result.push_str(&doc[..line_start]);
122 result.push_str(content.trim_end());
123 result.push('\n');
124 result.push_str(&doc[line_end..]);
125 return result;
126 }
127
128 self.append_with_caret(doc, content, None)
130 }
131}
132
133fn is_valid_name(name: &str) -> bool {
135 if name.is_empty() {
136 return false;
137 }
138 let first = name.as_bytes()[0];
139 if !first.is_ascii_alphanumeric() {
140 return false;
141 }
142 name.bytes()
143 .all(|b| b.is_ascii_alphanumeric() || b == b'-')
144}
145
146pub fn is_agent_marker(comment_text: &str) -> bool {
150 let trimmed = comment_text.trim();
151 if let Some(rest) = trimmed.strip_prefix("/agent:") {
152 is_valid_name(rest)
153 } else if let Some(rest) = trimmed.strip_prefix("agent:") {
154 let name_part = rest.split_whitespace().next().unwrap_or("");
156 is_valid_name(name_part)
157 } else {
158 false
159 }
160}
161
162fn parse_attrs(attr_text: &str) -> HashMap<String, String> {
167 let mut attrs = HashMap::new();
168 for token in attr_text.split_whitespace() {
169 if let Some((key, value)) = token.split_once('=')
170 && !key.is_empty()
171 && !value.is_empty()
172 {
173 attrs.insert(key.to_string(), value.to_string());
174 }
175 }
176 attrs
177}
178
179pub fn find_code_ranges(doc: &str) -> Vec<(usize, usize)> {
185 let mut ranges = Vec::new();
186 let parser = Parser::new_ext(doc, Options::empty());
187 let mut iter = parser.into_offset_iter();
188 while let Some((event, range)) = iter.next() {
189 match event {
190 Event::Code(_) => {
192 ranges.push((range.start, range.end));
193 }
194 Event::Start(Tag::CodeBlock(_)) => {
196 let block_start = range.start;
197 let mut block_end = range.end;
198 for (inner_event, inner_range) in iter.by_ref() {
199 block_end = inner_range.end;
200 if matches!(inner_event, Event::End(TagEnd::CodeBlock)) {
201 break;
202 }
203 }
204 ranges.push((block_start, block_end));
205 }
206 _ => {}
207 }
208 }
209 ranges
210}
211
212pub fn parse(doc: &str) -> Result<Vec<Component>> {
218 let bytes = doc.as_bytes();
219 let len = bytes.len();
220 let code_ranges = find_code_ranges(doc);
221 let mut templates: Vec<Component> = Vec::new();
222 let mut stack: Vec<(String, HashMap<String, String>, usize, usize)> = Vec::new();
224 let mut pos = 0;
225
226 while pos + 4 <= len {
227 if &bytes[pos..pos + 4] != b"<!--" {
229 pos += 1;
230 continue;
231 }
232
233 if code_ranges.iter().any(|&(start, end)| pos >= start && pos < end) {
235 pos += 4;
236 continue;
237 }
238
239 let marker_start = pos;
240
241 let close = match find_comment_end(bytes, pos + 4) {
243 Some(c) => c,
244 None => {
245 pos += 4;
246 continue;
247 }
248 };
249
250 let inner = &doc[marker_start + 4..close - 3]; let trimmed = inner.trim();
253
254 let mut marker_end = close;
256 if marker_end < len && bytes[marker_end] == b'\n' {
257 marker_end += 1;
258 }
259
260 if let Some(name) = trimmed.strip_prefix("/agent:") {
261 if !is_valid_name(name) {
263 bail!("invalid component name: '{}'", name);
264 }
265 match stack.pop() {
266 Some((open_name, open_attrs, open_start, open_end)) => {
267 if open_name != name {
268 bail!(
269 "mismatched component: opened '{}' but closed '{}'",
270 open_name,
271 name
272 );
273 }
274 templates.push(Component {
275 name: name.to_string(),
276 attrs: open_attrs,
277 open_start,
278 open_end,
279 close_start: marker_start,
280 close_end: marker_end,
281 });
282 }
283 None => bail!("closing marker <!-- /agent:{} --> without matching open", name),
284 }
285 } else if let Some(rest) = trimmed.strip_prefix("agent:") {
286 if rest.starts_with("boundary:") {
288 pos = close;
289 continue;
290 }
291 let mut parts = rest.splitn(2, |c: char| c.is_whitespace());
293 let name = parts.next().unwrap_or("");
294 let attr_text = parts.next().unwrap_or("");
295 if !is_valid_name(name) {
296 bail!("invalid component name: '{}'", name);
297 }
298 let attrs = parse_attrs(attr_text);
299 stack.push((name.to_string(), attrs, marker_start, marker_end));
300 }
301
302 pos = close;
303 }
304
305 if let Some((name, _, _, _)) = stack.last() {
306 bail!(
307 "unclosed component: <!-- agent:{} --> without matching close",
308 name
309 );
310 }
311
312 templates.sort_by_key(|t| t.open_start);
313 Ok(templates)
314}
315
316fn find_comment_end(bytes: &[u8], start: usize) -> Option<usize> {
318 let len = bytes.len();
319 let mut i = start;
320 while i + 3 <= len {
321 if &bytes[i..i + 3] == b"-->" {
322 return Some(i + 3);
323 }
324 i += 1;
325 }
326 None
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332
333 #[test]
334 fn single_range() {
335 let doc = "before\n<!-- agent:status -->\nHello\n<!-- /agent:status -->\nafter\n";
336 let ranges = parse(doc).unwrap();
337 assert_eq!(ranges.len(), 1);
338 assert_eq!(ranges[0].name, "status");
339 assert_eq!(ranges[0].content(doc), "Hello\n");
340 }
341
342 #[test]
343 fn nested_ranges() {
344 let doc = "\
345<!-- agent:outer -->
346<!-- agent:inner -->
347content
348<!-- /agent:inner -->
349<!-- /agent:outer -->
350";
351 let ranges = parse(doc).unwrap();
352 assert_eq!(ranges.len(), 2);
353 assert_eq!(ranges[0].name, "outer");
355 assert_eq!(ranges[1].name, "inner");
356 assert_eq!(ranges[1].content(doc), "content\n");
357 }
358
359 #[test]
360 fn siblings() {
361 let doc = "\
362<!-- agent:a -->
363alpha
364<!-- /agent:a -->
365<!-- agent:b -->
366beta
367<!-- /agent:b -->
368";
369 let ranges = parse(doc).unwrap();
370 assert_eq!(ranges.len(), 2);
371 assert_eq!(ranges[0].name, "a");
372 assert_eq!(ranges[0].content(doc), "alpha\n");
373 assert_eq!(ranges[1].name, "b");
374 assert_eq!(ranges[1].content(doc), "beta\n");
375 }
376
377 #[test]
378 fn no_ranges() {
379 let doc = "# Just a document\n\nWith no range templates.\n";
380 let ranges = parse(doc).unwrap();
381 assert!(ranges.is_empty());
382 }
383
384 #[test]
385 fn unmatched_open_error() {
386 let doc = "<!-- agent:orphan -->\nContent\n";
387 let err = parse(doc).unwrap_err();
388 assert!(err.to_string().contains("unclosed component"));
389 }
390
391 #[test]
392 fn unmatched_close_error() {
393 let doc = "Content\n<!-- /agent:orphan -->\n";
394 let err = parse(doc).unwrap_err();
395 assert!(err.to_string().contains("without matching open"));
396 }
397
398 #[test]
399 fn mismatched_names_error() {
400 let doc = "<!-- agent:foo -->\n<!-- /agent:bar -->\n";
401 let err = parse(doc).unwrap_err();
402 assert!(err.to_string().contains("mismatched"));
403 }
404
405 #[test]
406 fn invalid_name() {
407 let doc = "<!-- agent:-bad -->\n<!-- /agent:-bad -->\n";
408 let err = parse(doc).unwrap_err();
409 assert!(err.to_string().contains("invalid component name"));
410 }
411
412 #[test]
413 fn name_validation() {
414 assert!(is_valid_name("status"));
415 assert!(is_valid_name("my-section"));
416 assert!(is_valid_name("a1"));
417 assert!(is_valid_name("A"));
418 assert!(!is_valid_name(""));
419 assert!(!is_valid_name("-bad"));
420 assert!(!is_valid_name("has space"));
421 assert!(!is_valid_name("has_underscore"));
422 }
423
424 #[test]
425 fn content_extraction() {
426 let doc = "<!-- agent:x -->\nfoo\nbar\n<!-- /agent:x -->\n";
427 let ranges = parse(doc).unwrap();
428 assert_eq!(ranges[0].content(doc), "foo\nbar\n");
429 }
430
431 #[test]
432 fn replace_roundtrip() {
433 let doc = "before\n<!-- agent:s -->\nold\n<!-- /agent:s -->\nafter\n";
434 let ranges = parse(doc).unwrap();
435 let new_doc = ranges[0].replace_content(doc, "new\n");
436 assert_eq!(
437 new_doc,
438 "before\n<!-- agent:s -->\nnew\n<!-- /agent:s -->\nafter\n"
439 );
440 let ranges2 = parse(&new_doc).unwrap();
442 assert_eq!(ranges2.len(), 1);
443 assert_eq!(ranges2[0].content(&new_doc), "new\n");
444 }
445
446 #[test]
447 fn is_agent_marker_yes() {
448 assert!(is_agent_marker(" agent:status "));
449 assert!(is_agent_marker("/agent:status"));
450 assert!(is_agent_marker("agent:my-thing"));
451 assert!(is_agent_marker(" /agent:A1 "));
452 }
453
454 #[test]
455 fn is_agent_marker_no() {
456 assert!(!is_agent_marker("just a comment"));
457 assert!(!is_agent_marker("agent:"));
458 assert!(!is_agent_marker("/agent:"));
459 assert!(!is_agent_marker("agent:-bad"));
460 assert!(!is_agent_marker("some agent:fake stuff"));
461 }
462
463 #[test]
464 fn regular_comments_ignored() {
465 let doc = "<!-- just a comment -->\n<!-- agent:x -->\ndata\n<!-- /agent:x -->\n";
466 let ranges = parse(doc).unwrap();
467 assert_eq!(ranges.len(), 1);
468 assert_eq!(ranges[0].name, "x");
469 }
470
471 #[test]
472 fn multiline_comment_ignored() {
473 let doc = "\
474<!--
475multi
476line
477comment
478-->
479<!-- agent:s -->
480content
481<!-- /agent:s -->
482";
483 let ranges = parse(doc).unwrap();
484 assert_eq!(ranges.len(), 1);
485 assert_eq!(ranges[0].name, "s");
486 }
487
488 #[test]
489 fn empty_content() {
490 let doc = "<!-- agent:empty --><!-- /agent:empty -->\n";
491 let ranges = parse(doc).unwrap();
492 assert_eq!(ranges.len(), 1);
493 assert_eq!(ranges[0].content(doc), "");
494 }
495
496 #[test]
497 fn markers_in_fenced_code_block_ignored() {
498 let doc = "\
499<!-- agent:real -->
500content
501<!-- /agent:real -->
502```markdown
503<!-- agent:fake -->
504this is just an example
505<!-- /agent:fake -->
506```
507";
508 let ranges = parse(doc).unwrap();
509 assert_eq!(ranges.len(), 1);
510 assert_eq!(ranges[0].name, "real");
511 }
512
513 #[test]
514 fn markers_in_inline_code_ignored() {
515 let doc = "\
516Use `<!-- agent:example -->` markers for components.
517<!-- agent:real -->
518content
519<!-- /agent:real -->
520";
521 let ranges = parse(doc).unwrap();
522 assert_eq!(ranges.len(), 1);
523 assert_eq!(ranges[0].name, "real");
524 }
525
526 #[test]
527 fn markers_in_tilde_fence_ignored() {
528 let doc = "\
529<!-- agent:x -->
530data
531<!-- /agent:x -->
532~~~
533<!-- agent:y -->
534example
535<!-- /agent:y -->
536~~~
537";
538 let ranges = parse(doc).unwrap();
539 assert_eq!(ranges.len(), 1);
540 assert_eq!(ranges[0].name, "x");
541 }
542
543 #[test]
544 fn markers_in_indented_fenced_code_block_ignored() {
545 let doc = "\
547<!-- agent:exchange -->
548Content here.
549<!-- /agent:exchange -->
550
551 ```markdown
552 <!-- agent:fake -->
553 demo without closing tag
554 ```
555";
556 let ranges = parse(doc).unwrap();
557 assert_eq!(ranges.len(), 1);
558 assert_eq!(ranges[0].name, "exchange");
559 }
560
561 #[test]
562 fn indented_fence_inside_component_ignored() {
563 let doc = "\
565<!-- agent:exchange -->
566Here's how to set up:
567
568 ```markdown
569 <!-- agent:status -->
570 Your status here
571 ```
572
573Done explaining.
574<!-- /agent:exchange -->
575";
576 let ranges = parse(doc).unwrap();
577 assert_eq!(ranges.len(), 1);
578 assert_eq!(ranges[0].name, "exchange");
579 }
580
581 #[test]
582 fn deeply_indented_fence_ignored() {
583 let doc = "\
585<!-- agent:x -->
586ok
587<!-- /agent:x -->
588 ```
589 <!-- agent:y -->
590 inside fence
591 ```
592";
593 let ranges = parse(doc).unwrap();
594 assert_eq!(ranges.len(), 1);
595 assert_eq!(ranges[0].name, "x");
596 }
597
598 #[test]
599 fn indented_fence_code_ranges_detected() {
600 let doc = "before\n ```\n code\n ```\nafter\n";
601 let ranges = find_code_ranges(doc);
602 assert_eq!(ranges.len(), 1);
603 assert!(doc[ranges[0].0..ranges[0].1].contains("code"));
604 }
605
606 #[test]
607 fn code_ranges_detected() {
608 let doc = "before\n```\ncode\n```\nafter `inline` end\n";
609 let ranges = find_code_ranges(doc);
610 assert_eq!(ranges.len(), 2);
611 assert!(doc[ranges[0].0..ranges[0].1].contains("code"));
613 assert!(doc[ranges[1].0..ranges[1].1].contains("inline"));
615 }
616
617 #[test]
618 fn code_ranges_double_backtick() {
619 let doc = "text `` `<!--` `` more\n";
621 let ranges = find_code_ranges(doc);
622 assert_eq!(ranges.len(), 1);
623 let span = &doc[ranges[0].0..ranges[0].1];
624 assert!(span.contains("<!--"), "double-backtick span should contain <!--: {:?}", span);
625 }
626
627 #[test]
628 fn code_ranges_double_backtick_does_not_match_single() {
629 let doc = "text `` foo ` bar `` end\n";
631 let ranges = find_code_ranges(doc);
632 assert_eq!(ranges.len(), 1);
633 let span = &doc[ranges[0].0..ranges[0].1];
634 assert_eq!(span, "`` foo ` bar ``");
635 }
636
637 #[test]
638 fn double_backtick_comment_before_agent_marker() {
639 let doc = "\
641<!-- agent:exchange -->\n\
642text `` `<!--` `` description\n\
643new content here\n\
644<!-- /agent:exchange -->\n";
645 let components = parse(doc).unwrap();
646 assert_eq!(components.len(), 1);
647 assert_eq!(components[0].name, "exchange");
648 assert!(components[0].content(doc).contains("new content here"));
649 }
650
651 #[test]
654 fn parse_component_with_mode_attr() {
655 let doc = "<!-- agent:exchange mode=append -->\nContent\n<!-- /agent:exchange -->\n";
656 let components = parse(doc).unwrap();
657 assert_eq!(components.len(), 1);
658 assert_eq!(components[0].name, "exchange");
659 assert_eq!(components[0].attrs.get("mode").map(|s| s.as_str()), Some("append"));
660 assert_eq!(components[0].content(doc), "Content\n");
661 }
662
663 #[test]
664 fn parse_component_with_multiple_attrs() {
665 let doc = "<!-- agent:log mode=prepend timestamp=true -->\nData\n<!-- /agent:log -->\n";
666 let components = parse(doc).unwrap();
667 assert_eq!(components.len(), 1);
668 assert_eq!(components[0].name, "log");
669 assert_eq!(components[0].attrs.get("mode").map(|s| s.as_str()), Some("prepend"));
670 assert_eq!(components[0].attrs.get("timestamp").map(|s| s.as_str()), Some("true"));
671 }
672
673 #[test]
674 fn parse_component_no_attrs_backward_compat() {
675 let doc = "<!-- agent:status -->\nOK\n<!-- /agent:status -->\n";
676 let components = parse(doc).unwrap();
677 assert_eq!(components.len(), 1);
678 assert_eq!(components[0].name, "status");
679 assert!(components[0].attrs.is_empty());
680 }
681
682 #[test]
683 fn is_agent_marker_with_attrs() {
684 assert!(is_agent_marker(" agent:exchange mode=append "));
685 assert!(is_agent_marker("agent:status mode=replace"));
686 assert!(is_agent_marker("agent:log mode=prepend timestamp=true"));
687 }
688
689 #[test]
690 fn closing_tag_unchanged_with_attrs() {
691 let doc = "<!-- agent:status mode=replace -->\n- [x] Done\n<!-- /agent:status -->\n";
693 let components = parse(doc).unwrap();
694 assert_eq!(components.len(), 1);
695 let new_doc = components[0].replace_content(doc, "- [ ] Todo\n");
696 assert!(new_doc.contains("<!-- agent:status mode=replace -->"));
697 assert!(new_doc.contains("<!-- /agent:status -->"));
698 assert!(new_doc.contains("- [ ] Todo"));
699 }
700
701 #[test]
702 fn parse_component_with_patch_attr() {
703 let doc = "<!-- agent:exchange patch=append -->\nContent\n<!-- /agent:exchange -->\n";
704 let components = parse(doc).unwrap();
705 assert_eq!(components.len(), 1);
706 assert_eq!(components[0].name, "exchange");
707 assert_eq!(components[0].patch_mode(), Some("append"));
708 assert_eq!(components[0].content(doc), "Content\n");
709 }
710
711 #[test]
712 fn patch_attr_takes_precedence_over_mode() {
713 let doc = "<!-- agent:exchange patch=replace mode=append -->\nContent\n<!-- /agent:exchange -->\n";
714 let components = parse(doc).unwrap();
715 assert_eq!(components[0].patch_mode(), Some("replace"));
716 }
717
718 #[test]
719 fn mode_attr_backward_compat() {
720 let doc = "<!-- agent:exchange mode=append -->\nContent\n<!-- /agent:exchange -->\n";
721 let components = parse(doc).unwrap();
722 assert_eq!(components[0].patch_mode(), Some("append"));
723 }
724
725 #[test]
726 fn no_patch_or_mode_attr() {
727 let doc = "<!-- agent:exchange -->\nContent\n<!-- /agent:exchange -->\n";
728 let components = parse(doc).unwrap();
729 assert_eq!(components[0].patch_mode(), None);
730 }
731
732 #[test]
735 fn single_backtick_component_tag_ignored() {
736 let doc = "\
738Use `<!-- agent:pending patch=replace -->` to mark pending sections.
739<!-- agent:real -->
740content
741<!-- /agent:real -->
742";
743 let components = parse(doc).unwrap();
744 assert_eq!(components.len(), 1);
745 assert_eq!(components[0].name, "real");
746 }
747
748 #[test]
749 fn double_backtick_component_tag_ignored() {
750 let doc = "\
752Use ``<!-- agent:pending patch=replace -->`` to mark pending sections.
753<!-- agent:real -->
754content
755<!-- /agent:real -->
756";
757 let components = parse(doc).unwrap();
758 assert_eq!(components.len(), 1);
759 assert_eq!(components[0].name, "real");
760 }
761
762 #[test]
763 fn component_tags_not_in_backticks_still_work() {
764 let doc = "\
766<!-- agent:a -->
767alpha
768<!-- /agent:a -->
769<!-- agent:b patch=append -->
770beta
771<!-- /agent:b -->
772";
773 let components = parse(doc).unwrap();
774 assert_eq!(components.len(), 2);
775 assert_eq!(components[0].name, "a");
776 assert_eq!(components[1].name, "b");
777 assert_eq!(components[1].patch_mode(), Some("append"));
778 }
779
780 #[test]
781 fn mixed_backtick_and_real_tags() {
782 let doc = "\
784Here is an example: `<!-- agent:fake -->` and ``<!-- /agent:fake -->``.
785<!-- agent:real -->
786real content
787<!-- /agent:real -->
788Another example: `<!-- agent:also-fake patch=replace -->` is just documentation.
789";
790 let components = parse(doc).unwrap();
791 assert_eq!(components.len(), 1);
792 assert_eq!(components[0].name, "real");
793 assert_eq!(components[0].content(doc), "real content\n");
794 }
795
796 #[test]
797 fn inline_code_mid_line_with_surrounding_text_ignored() {
798 let doc = "\
801Wrap markers like `<!-- agent:status -->` in backticks to show them literally.
802<!-- agent:real -->
803actual content
804<!-- /agent:real -->
805";
806 let components = parse(doc).unwrap();
807 assert_eq!(components.len(), 1);
808 assert_eq!(components[0].name, "real");
809 assert_eq!(components[0].content(doc), "actual content\n");
810 }
811
812 #[test]
813 fn parse_attrs_unit() {
814 let attrs = parse_attrs("mode=append");
815 assert_eq!(attrs.get("mode").map(|s| s.as_str()), Some("append"));
816
817 let attrs = parse_attrs("mode=replace timestamp=true");
818 assert_eq!(attrs.len(), 2);
819
820 let attrs = parse_attrs("");
821 assert!(attrs.is_empty());
822
823 let attrs = parse_attrs("mode=append broken novalue=");
825 assert_eq!(attrs.len(), 1);
826 assert_eq!(attrs.get("mode").map(|s| s.as_str()), Some("append"));
827 }
828}