1use std::ops::Range;
2
3use crate::parser::utils::yaml_regions::hashpipe_language_and_prefix;
4use crate::parser::yaml::YamlDiagnostic;
5use crate::syntax::{
6 AstNode, PanacheLanguage, SyntaxKind, SyntaxNode, YamlDocument, YamlScalarStyle,
7};
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct YamlFrontmatterRegion {
11 pub id: String,
12 pub host_range: Range<usize>,
13 pub content_range: Range<usize>,
14 pub content: String,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum YamlRegionKind {
19 Frontmatter,
20 Hashpipe,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct YamlRegion {
25 pub id: String,
26 pub kind: YamlRegionKind,
27 pub host_range: Range<usize>,
28 pub region_range: Range<usize>,
29 pub content_range: Range<usize>,
30 pub content: String,
31}
32
33#[derive(Debug, Clone)]
34pub struct ParsedYamlRegion {
35 region: YamlRegion,
36 embedded: Option<SyntaxNode>,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct ParsedYamlRegionSnapshot {
45 region: YamlRegion,
46 parse_ok: bool,
47 document_shape_summary: Option<String>,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum YamlEmbeddingHostKind {
52 FrontmatterMetadata,
53 HashpipePreamble,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Hash)]
57pub struct YamlMetadata(SyntaxNode);
58
59impl AstNode for YamlMetadata {
60 type Language = PanacheLanguage;
61
62 fn can_cast(kind: SyntaxKind) -> bool {
63 kind == SyntaxKind::YAML_METADATA
64 }
65
66 fn cast(node: SyntaxNode) -> Option<Self> {
67 Self::can_cast(node.kind()).then(|| Self(node))
68 }
69
70 fn syntax(&self) -> &SyntaxNode {
71 &self.0
72 }
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, Hash)]
76pub struct HashpipeYamlPreamble(SyntaxNode);
77
78impl AstNode for HashpipeYamlPreamble {
79 type Language = PanacheLanguage;
80
81 fn can_cast(kind: SyntaxKind) -> bool {
82 kind == SyntaxKind::HASHPIPE_YAML_PREAMBLE
83 }
84
85 fn cast(node: SyntaxNode) -> Option<Self> {
86 Self::can_cast(node.kind()).then(|| Self(node))
87 }
88
89 fn syntax(&self) -> &SyntaxNode {
90 &self.0
91 }
92}
93
94#[derive(Debug, Clone)]
95pub enum YamlEmbeddingHost {
96 FrontmatterMetadata(YamlMetadata),
97 HashpipePreamble(HashpipeYamlPreamble),
98}
99
100#[derive(Debug, Clone)]
101pub struct YamlEmbeddedCst {
102 host: YamlEmbeddingHost,
103 parsed: ParsedYamlRegion,
104}
105
106impl YamlEmbeddedCst {
107 pub fn host_kind(&self) -> YamlEmbeddingHostKind {
108 match self.host {
109 YamlEmbeddingHost::FrontmatterMetadata(_) => YamlEmbeddingHostKind::FrontmatterMetadata,
110 YamlEmbeddingHost::HashpipePreamble(_) => YamlEmbeddingHostKind::HashpipePreamble,
111 }
112 }
113
114 pub fn host_node(&self) -> &SyntaxNode {
115 match &self.host {
116 YamlEmbeddingHost::FrontmatterMetadata(host) => host.syntax(),
117 YamlEmbeddingHost::HashpipePreamble(host) => host.syntax(),
118 }
119 }
120
121 pub fn frontmatter_host(&self) -> Option<&YamlMetadata> {
122 match &self.host {
123 YamlEmbeddingHost::FrontmatterMetadata(host) => Some(host),
124 _ => None,
125 }
126 }
127
128 pub fn hashpipe_host(&self) -> Option<&HashpipeYamlPreamble> {
129 match &self.host {
130 YamlEmbeddingHost::HashpipePreamble(host) => Some(host),
131 _ => None,
132 }
133 }
134
135 pub fn parsed(&self) -> &ParsedYamlRegion {
136 &self.parsed
137 }
138
139 pub fn yaml_content(&self) -> &str {
140 self.parsed.content()
141 }
142}
143
144#[derive(Debug, Clone, Copy, PartialEq, Eq)]
145pub enum YamlAstRootKind {
146 Root,
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq)]
150pub enum YamlDocumentKind {
151 BlockMap,
152 BlockSeq,
153 BlockScalar,
154 Flow,
155 Empty,
156}
157
158#[derive(Debug, Clone, Copy)]
159pub struct YamlAstRoot<'a> {
160 node: &'a SyntaxNode,
161}
162
163impl YamlAstRoot<'_> {
164 pub fn kind(&self) -> YamlAstRootKind {
165 YamlAstRootKind::Root
166 }
167
168 pub fn document_count(&self) -> usize {
169 self.documents().count()
170 }
171
172 pub fn first_document_kind(&self) -> Option<YamlDocumentKind> {
173 let doc = self.documents().next()?;
174 if doc.block_map().is_some() {
175 return Some(YamlDocumentKind::BlockMap);
176 }
177 if doc.block_sequence().is_some() {
178 return Some(YamlDocumentKind::BlockSeq);
179 }
180 if let Some(scalar) = doc.scalar() {
181 return Some(match scalar.style() {
182 YamlScalarStyle::Literal | YamlScalarStyle::Folded => YamlDocumentKind::BlockScalar,
183 _ => YamlDocumentKind::Flow,
184 });
185 }
186 if doc.flow_map().is_some() || doc.flow_sequence().is_some() {
187 return Some(YamlDocumentKind::Flow);
188 }
189 Some(YamlDocumentKind::Empty)
190 }
191
192 fn documents(&self) -> impl Iterator<Item = YamlDocument> + '_ {
193 self.node.descendants().filter_map(YamlDocument::cast)
194 }
195}
196
197#[derive(Debug, Clone, PartialEq, Eq)]
198pub struct YamlParseError {
199 offset: usize,
200 message: String,
201}
202
203impl YamlParseError {
204 pub fn offset(&self) -> usize {
205 self.offset
206 }
207
208 pub fn message(&self) -> &str {
209 &self.message
210 }
211
212 fn from_diagnostic(diag: &YamlDiagnostic) -> Self {
213 Self {
214 offset: diag.byte_start,
215 message: diag.message.to_string(),
216 }
217 }
218}
219
220impl ParsedYamlRegion {
221 pub fn id(&self) -> &str {
222 &self.region.id
223 }
224
225 pub fn kind(&self) -> &YamlRegionKind {
226 &self.region.kind
227 }
228
229 pub fn is_frontmatter(&self) -> bool {
230 matches!(self.region.kind, YamlRegionKind::Frontmatter)
231 }
232
233 pub fn is_hashpipe(&self) -> bool {
234 matches!(self.region.kind, YamlRegionKind::Hashpipe)
235 }
236
237 pub fn root(&self) -> Option<YamlAstRoot<'_>> {
241 self.embedded.as_ref().map(|node| YamlAstRoot { node })
242 }
243
244 pub fn root_kind(&self) -> Option<YamlAstRootKind> {
245 self.root().map(|root| root.kind())
246 }
247
248 pub fn is_valid(&self) -> bool {
252 self.embedded.is_some()
253 }
254
255 pub fn host_range(&self) -> Range<usize> {
256 self.region.host_range.clone()
257 }
258
259 pub fn content_range(&self) -> Range<usize> {
260 self.region.content_range.clone()
261 }
262
263 pub fn region_range(&self) -> Range<usize> {
264 self.region.region_range.clone()
265 }
266
267 pub fn to_region(&self) -> YamlRegion {
268 self.region.clone()
269 }
270
271 pub fn content(&self) -> &str {
272 &self.region.content
273 }
274
275 pub fn document_shape_summary(&self) -> Option<String> {
276 let root = self.root()?;
277 let doc_count = root.document_count();
278 let first_kind = root.first_document_kind();
279 Some(match first_kind {
280 Some(kind) => format!("{:?} docs={} first={:?}", root.kind(), doc_count, kind),
281 None => format!("{:?} docs={}", root.kind(), doc_count),
282 })
283 }
284
285 pub fn to_snapshot(&self) -> ParsedYamlRegionSnapshot {
286 ParsedYamlRegionSnapshot {
287 region: self.region.clone(),
288 parse_ok: self.is_valid(),
289 document_shape_summary: self.document_shape_summary(),
290 }
291 }
292}
293
294impl ParsedYamlRegionSnapshot {
295 pub fn id(&self) -> &str {
296 &self.region.id
297 }
298
299 pub fn is_frontmatter(&self) -> bool {
300 matches!(self.region.kind, YamlRegionKind::Frontmatter)
301 }
302
303 pub fn is_hashpipe(&self) -> bool {
304 matches!(self.region.kind, YamlRegionKind::Hashpipe)
305 }
306
307 pub fn is_valid(&self) -> bool {
308 self.parse_ok
309 }
310
311 pub fn host_range(&self) -> Range<usize> {
312 self.region.host_range.clone()
313 }
314
315 pub fn document_shape_summary(&self) -> Option<&str> {
316 self.document_shape_summary.as_deref()
317 }
318
319 pub fn to_region(&self) -> YamlRegion {
320 self.region.clone()
321 }
322}
323
324pub fn collect_frontmatter_region(tree: &SyntaxNode) -> Option<YamlFrontmatterRegion> {
325 let metadata = tree
326 .descendants()
327 .find(|node| node.kind() == SyntaxKind::YAML_METADATA)?;
328 let content_node = metadata
329 .children()
330 .find(|child| child.kind() == SyntaxKind::YAML_METADATA_CONTENT)?;
331
332 let host_start: usize = metadata.text_range().start().into();
333 let host_end: usize = metadata.text_range().end().into();
334 let content_start: usize = content_node.text_range().start().into();
335 let content_end: usize = content_node.text_range().end().into();
336
337 Some(YamlFrontmatterRegion {
338 id: format!("frontmatter:{}:{}", content_start, content_end),
339 host_range: host_start..host_end,
340 content_range: content_start..content_end,
341 content: content_node.text().to_string(),
342 })
343}
344
345pub fn collect_frontmatter_yaml_region(tree: &SyntaxNode) -> Option<YamlRegion> {
346 let frontmatter = collect_frontmatter_region(tree)?;
347 let content_range = frontmatter.content_range.clone();
348 Some(YamlRegion {
349 id: frontmatter.id,
350 kind: YamlRegionKind::Frontmatter,
351 host_range: frontmatter.host_range.clone(),
352 region_range: frontmatter.host_range,
353 content_range,
354 content: frontmatter.content,
355 })
356}
357
358pub fn collect_hashpipe_regions(tree: &SyntaxNode) -> Vec<YamlRegion> {
359 let mut regions = Vec::new();
360 for node in tree
361 .descendants()
362 .filter(|n| n.kind() == SyntaxKind::CODE_BLOCK)
363 {
364 let mut info_text: Option<String> = None;
365 let mut content_node: Option<SyntaxNode> = None;
366 for child in node.children() {
367 match child.kind() {
368 SyntaxKind::CODE_FENCE_OPEN => {
369 for nested in child.children() {
370 if nested.kind() == SyntaxKind::CODE_INFO {
371 info_text = Some(nested.text().to_string());
372 }
373 }
374 }
375 SyntaxKind::CODE_CONTENT => content_node = Some(child),
376 _ => {}
377 }
378 }
379 let (Some(info_text), Some(content_node)) = (info_text, content_node) else {
380 continue;
381 };
382 let Some((language, prefix)) = hashpipe_language_and_prefix(&info_text) else {
383 continue;
384 };
385
386 let host_start: usize = node.text_range().start().into();
387 let host_end: usize = node.text_range().end().into();
388 let Some(preamble) = content_node
389 .children()
390 .find(|n| n.kind() == SyntaxKind::HASHPIPE_YAML_PREAMBLE)
391 else {
392 continue;
393 };
394 let Some(preamble_content) = preamble
395 .children()
396 .find(|n| n.kind() == SyntaxKind::HASHPIPE_YAML_CONTENT)
397 else {
398 continue;
399 };
400 let preamble_text = preamble_content.text().to_string();
401 let preamble_start: usize = preamble_content.text_range().start().into();
402 if let Some(region) = extract_hashpipe_region(
403 &preamble_text,
404 host_start,
405 host_end,
406 preamble_start,
407 prefix,
408 language.as_str(),
409 ) {
410 regions.push(region);
411 }
412 }
413 regions
414}
415
416pub fn collect_yaml_regions(tree: &SyntaxNode) -> Vec<YamlRegion> {
417 let mut regions = Vec::new();
418 if let Some(frontmatter) = collect_frontmatter_yaml_region(tree) {
419 regions.push(frontmatter);
420 }
421 regions.extend(collect_hashpipe_regions(tree));
422 regions
423}
424
425pub fn collect_parsed_yaml_regions(tree: &SyntaxNode) -> Vec<ParsedYamlRegion> {
426 let embedded_frontmatter = embedded_frontmatter_stream(tree);
427 collect_yaml_regions(tree)
428 .into_iter()
429 .map(|region| {
430 let embedded = match ®ion.kind {
435 YamlRegionKind::Frontmatter => embedded_frontmatter.clone(),
436 YamlRegionKind::Hashpipe => embedded_hashpipe_stream(tree, ®ion.region_range),
437 };
438 ParsedYamlRegion { embedded, region }
439 })
440 .collect()
441}
442
443fn embedded_frontmatter_stream(tree: &SyntaxNode) -> Option<SyntaxNode> {
451 let metadata = tree
452 .descendants()
453 .find(|node| node.kind() == SyntaxKind::YAML_METADATA)?;
454 let content_node = metadata
455 .children()
456 .find(|child| child.kind() == SyntaxKind::YAML_METADATA_CONTENT)?;
457 (!is_opaque_yaml_fallback(&content_node)).then_some(content_node)
458}
459
460fn embedded_hashpipe_stream(tree: &SyntaxNode, region_range: &Range<usize>) -> Option<SyntaxNode> {
466 tree.descendants()
467 .filter(|node| node.kind() == SyntaxKind::HASHPIPE_YAML_CONTENT)
468 .find(|node| {
469 let start: usize = node.text_range().start().into();
470 let end: usize = node.text_range().end().into();
471 start == region_range.start && end == region_range.end
472 })
473 .filter(|node| !is_opaque_yaml_fallback(node))
474}
475
476fn is_opaque_yaml_fallback(content_node: &SyntaxNode) -> bool {
483 content_node
484 .children_with_tokens()
485 .any(|element| element.kind() == SyntaxKind::TEXT)
486}
487
488pub fn collect_parsed_frontmatter_region(tree: &SyntaxNode) -> Option<ParsedYamlRegion> {
489 collect_parsed_yaml_regions(tree)
490 .into_iter()
491 .find(|region| region.is_frontmatter())
492}
493
494pub fn collect_parsed_yaml_region_snapshots(tree: &SyntaxNode) -> Vec<ParsedYamlRegionSnapshot> {
495 collect_parsed_yaml_regions(tree)
496 .iter()
497 .map(ParsedYamlRegion::to_snapshot)
498 .collect()
499}
500
501pub fn validate_yaml_text(input: &str) -> Result<(), YamlParseError> {
502 match crate::parser::yaml::parse_yaml_report(input)
503 .diagnostics
504 .first()
505 {
506 Some(diag) => Err(YamlParseError::from_diagnostic(diag)),
507 None => Ok(()),
508 }
509}
510
511pub fn collect_embedded_yaml_cst(tree: &SyntaxNode) -> Vec<YamlEmbeddedCst> {
512 let parsed_regions = collect_parsed_yaml_regions(tree);
513 let frontmatter_node = tree.descendants().find_map(YamlMetadata::cast);
514 let hashpipe_preambles: Vec<HashpipeYamlPreamble> = tree
515 .descendants()
516 .filter_map(HashpipeYamlPreamble::cast)
517 .collect();
518
519 let mut embedded = Vec::new();
520 for parsed in parsed_regions {
521 match parsed.kind() {
522 YamlRegionKind::Frontmatter => {
523 if let Some(node) = frontmatter_node.clone() {
524 embedded.push(YamlEmbeddedCst {
525 host: YamlEmbeddingHost::FrontmatterMetadata(node),
526 parsed,
527 });
528 }
529 }
530 YamlRegionKind::Hashpipe => {
531 if let Some(node) = hashpipe_preambles.iter().find(|node| {
532 let range: Range<usize> = node.syntax().text_range().start().into()
533 ..node.syntax().text_range().end().into();
534 range == parsed.region_range()
535 }) {
536 embedded.push(YamlEmbeddedCst {
537 host: YamlEmbeddingHost::HashpipePreamble(node.clone()),
538 parsed,
539 });
540 }
541 }
542 }
543 }
544 embedded
545}
546
547pub fn collect_embedded_frontmatter_yaml_cst(tree: &SyntaxNode) -> Option<YamlEmbeddedCst> {
548 collect_embedded_yaml_cst(tree)
549 .into_iter()
550 .find(|embedded| embedded.frontmatter_host().is_some())
551}
552
553fn extract_hashpipe_region(
554 content: &str,
555 host_start: usize,
556 host_end: usize,
557 content_start: usize,
558 prefix: &str,
559 language: &str,
560) -> Option<YamlRegion> {
561 let lines: Vec<&str> = content.split_inclusive('\n').collect();
562 if lines.is_empty() {
563 return None;
564 }
565 let mut collected = String::new();
570 let mut offset = 0usize;
571 for line in &lines {
572 let line = *line;
573 let line_core = line.strip_suffix('\n').unwrap_or(line);
574 let line_core = line_core.strip_suffix('\r').unwrap_or(line_core);
575 let eol = &line[line_core.len()..];
576 let indent_len = line_core
577 .chars()
578 .take_while(|ch| *ch == ' ' || *ch == '\t')
579 .map(char::len_utf8)
580 .sum::<usize>();
581 let trimmed = &line_core[indent_len..];
582 let after_prefix = trimmed.strip_prefix(prefix)?;
583 let payload = after_prefix
584 .strip_prefix(' ')
585 .or_else(|| after_prefix.strip_prefix('\t'))
586 .unwrap_or(after_prefix);
587 collected.push_str(payload);
588 collected.push_str(eol);
589 offset += line.len();
590 }
591 let start = content_start;
592 let region_end = content_start + offset;
593 let id = format!("hashpipe:{}:{}:{}", language, host_start, start);
594 Some(YamlRegion {
595 id,
596 kind: YamlRegionKind::Hashpipe,
597 host_range: host_start..host_end,
598 region_range: start..region_end,
599 content_range: start..region_end,
600 content: collected,
601 })
602}
603
604#[cfg(test)]
605mod tests {
606 use super::*;
607
608 #[test]
609 fn parsed_yaml_regions_include_frontmatter_and_hashpipe_cst_roots() {
610 let input = "---\ntitle: Test\n---\n\n```{r}\n#| echo: false\n1 + 1\n```\n";
611 let config = crate::options::ParserOptions {
612 flavor: crate::options::Flavor::Quarto,
613 extensions: crate::options::Extensions::for_flavor(crate::options::Flavor::Quarto),
614 ..Default::default()
615 };
616 let tree = crate::parser::parse(input, Some(config));
617 let regions = collect_parsed_yaml_regions(&tree);
618 assert_eq!(regions.len(), 2);
619 assert!(regions.iter().any(|parsed| {
620 parsed.is_frontmatter() && parsed.root_kind() == Some(YamlAstRootKind::Root)
621 }));
622 assert!(regions.iter().any(|parsed| {
623 parsed.is_hashpipe() && parsed.root_kind() == Some(YamlAstRootKind::Root)
624 }));
625 }
626
627 #[test]
628 fn parsed_hashpipe_region_validity_derives_from_embedded_subtree() {
629 let config = crate::options::ParserOptions {
630 flavor: crate::options::Flavor::Quarto,
631 extensions: crate::options::Extensions::for_flavor(crate::options::Flavor::Quarto),
632 ..Default::default()
633 };
634 let bad = crate::parser::parse("```{r}\n#| echo: [\n1 + 1\n```\n", Some(config.clone()));
636 let bad_region = collect_parsed_yaml_regions(&bad)
637 .into_iter()
638 .find(|region| region.is_hashpipe())
639 .expect("hashpipe region");
640 assert!(!bad_region.is_valid());
641 assert!(bad_region.root().is_none());
642
643 let good = crate::parser::parse("```{r}\n#| echo: false\n1 + 1\n```\n", Some(config));
645 let good_region = collect_parsed_yaml_regions(&good)
646 .into_iter()
647 .find(|region| region.is_hashpipe())
648 .expect("hashpipe region");
649 assert!(good_region.is_valid());
650 assert!(good_region.root().is_some());
651 }
652
653 #[test]
654 fn empty_frontmatter_is_valid() {
655 let tree = crate::parser::parse("---\n---\n\nbody\n", None);
658 let parsed = collect_parsed_frontmatter_region(&tree).expect("frontmatter");
659 assert!(parsed.is_valid());
660 }
661
662 #[test]
663 fn malformed_frontmatter_is_invalid() {
664 let tree = crate::parser::parse("---\ntitle: [\n---\n", None);
665 let parsed = collect_parsed_frontmatter_region(&tree).expect("frontmatter");
666 assert!(!parsed.is_valid());
667 }
668
669 #[test]
670 fn yaml_ast_root_reports_document_shape() {
671 let input = "---\ntitle: Test\n---\n";
672 let tree = crate::parser::parse(input, None);
673 let parsed = collect_parsed_frontmatter_region(&tree).expect("frontmatter");
674 let root = parsed.root().expect("yaml root");
675 assert_eq!(root.document_count(), 1);
676 assert_eq!(root.first_document_kind(), Some(YamlDocumentKind::BlockMap));
677 }
678
679 #[test]
680 fn embedded_yaml_cst_attaches_to_frontmatter_and_hashpipe_hosts() {
681 let input = "---\ntitle: Test\n---\n\n```{r}\n#| echo: false\nx <- 1\n```\n";
682 let config = crate::options::ParserOptions {
683 flavor: crate::options::Flavor::Quarto,
684 extensions: crate::options::Extensions::for_flavor(crate::options::Flavor::Quarto),
685 ..Default::default()
686 };
687 let tree = crate::parser::parse(input, Some(config));
688 let embedded = collect_embedded_yaml_cst(&tree);
689 assert_eq!(embedded.len(), 2);
690 assert!(
691 embedded
692 .iter()
693 .any(|item| item.frontmatter_host().is_some())
694 );
695 assert!(embedded.iter().any(|item| item.hashpipe_host().is_some()));
696 }
697
698 #[test]
699 fn embedded_yaml_cst_exposes_frontmatter_and_hashpipe_payloads() {
700 let input = "---\ntitle: Test\n---\n\n```{r}\n#| fig-cap: |\n#| A caption\nx <- 1\n```\n";
701 let config = crate::options::ParserOptions {
702 flavor: crate::options::Flavor::Quarto,
703 extensions: crate::options::Extensions::for_flavor(crate::options::Flavor::Quarto),
704 ..Default::default()
705 };
706 let tree = crate::parser::parse(input, Some(config));
707 let embedded = collect_embedded_yaml_cst(&tree);
708 assert_eq!(embedded.len(), 2);
709
710 let frontmatter = embedded
711 .iter()
712 .find(|item| item.frontmatter_host().is_some())
713 .expect("frontmatter embedding");
714 assert!(frontmatter.parsed().is_valid());
715 assert_eq!(
716 frontmatter.parsed().document_shape_summary().as_deref(),
717 Some("Root docs=1 first=BlockMap")
718 );
719
720 let hashpipe = embedded
721 .iter()
722 .find(|item| item.hashpipe_host().is_some())
723 .expect("hashpipe embedding");
724 assert!(hashpipe.parsed().is_valid());
725 assert!(hashpipe.parsed().to_region().content.contains("fig-cap: |"));
726 }
727
728 #[test]
729 fn embedded_frontmatter_query_returns_typed_host_wrapper() {
730 let input = "---\ntitle: Test\n---\n\nBody\n";
731 let tree = crate::parser::parse(input, None);
732 let embedded = collect_embedded_frontmatter_yaml_cst(&tree).expect("frontmatter embedding");
733 let _host = embedded.frontmatter_host().expect("frontmatter host");
734 assert!(embedded.hashpipe_host().is_none());
735 }
736}