yaml_edit/nodes/comment.rs
1//! Comment trivia.
2
3use super::{fresh_token, SyntaxNode, SyntaxToken};
4use crate::lex::SyntaxKind;
5
6/// A single comment token in a YAML document.
7///
8/// Comments are trivia: they are preserved in the syntax tree but do not affect
9/// the parsed value. Use [`YamlFile::comments`](crate::YamlFile::comments) or
10/// [`Document::comments`](crate::Document::comments) to iterate over them.
11#[derive(Clone, PartialEq, Eq, Hash)]
12pub struct Comment(pub(crate) SyntaxToken);
13
14impl Comment {
15 /// Wrap a syntax token as a comment, if it is a `COMMENT` token.
16 pub(crate) fn cast(token: SyntaxToken) -> Option<Self> {
17 (token.kind() == SyntaxKind::COMMENT).then_some(Comment(token))
18 }
19
20 /// The full comment text, including the leading `#`.
21 ///
22 /// For `# hello` this returns `"# hello"`.
23 pub fn text(&self) -> &str {
24 self.0.text()
25 }
26
27 /// The comment content with the leading `#` and a single optional space removed.
28 ///
29 /// For `# hello` this returns `"hello"`; for `#hello` it returns `"hello"`.
30 /// Trailing whitespace is left intact.
31 pub fn content(&self) -> &str {
32 let text = self.0.text();
33 let without_hash = text.strip_prefix('#').unwrap_or(text);
34 without_hash.strip_prefix(' ').unwrap_or(without_hash)
35 }
36
37 crate::nodes::ast_node_spans!("comment");
38
39 /// Get the text range of this comment as a rowan [`TextRange`](rowan::TextRange).
40 ///
41 /// The range spans the full comment text, including the leading `#`.
42 pub fn text_range(&self) -> rowan::TextRange {
43 self.0.text_range()
44 }
45
46 /// Replace the full comment text, including the leading `#`.
47 ///
48 /// # Errors
49 ///
50 /// Returns a [`CommentTextError`], leaving the tree unchanged, if `text`
51 /// does not start with `#` or contains a line break, either of which
52 /// would turn the token into something other than a single comment, or
53 /// a character YAML does not allow in a stream.
54 ///
55 /// Afterwards this handle refers to the new comment token.
56 ///
57 /// # Example
58 /// ```
59 /// use yaml_edit::Document;
60 /// use std::str::FromStr;
61 ///
62 /// let doc = Document::from_str("key: value # teh value\n").unwrap();
63 /// let mut comment = doc.comments().next().unwrap();
64 /// comment.set_text("# the value").unwrap();
65 /// assert_eq!(doc.to_string(), "key: value # the value\n");
66 /// ```
67 pub fn set_text(&mut self, text: &str) -> Result<(), CommentTextError> {
68 if !text.starts_with('#') {
69 return Err(CommentTextError::MissingHash);
70 }
71 if text.contains(['\n', '\r']) {
72 return Err(CommentTextError::LineBreak);
73 }
74 if let Some(c) = text.chars().find(|&c| !crate::lex::is_printable(c)) {
75 return Err(CommentTextError::NonPrintable(c));
76 }
77 let parent = self
78 .0
79 .parent()
80 .expect("a comment token always has a parent node");
81 let index = self.0.index();
82 let new_token = fresh_token(SyntaxKind::COMMENT, text);
83 super::replace_children(&parent, index..index + 1, vec![new_token.into()]);
84 self.0 = parent
85 .children_with_tokens()
86 .nth(index)
87 .and_then(|element| element.into_token())
88 .expect("the replacement comment sits at the old comment's index");
89 Ok(())
90 }
91
92 /// Replace the comment content, preserving the leading `# ` marker.
93 ///
94 /// This is the counterpart to [`content`](Self::content): it sets the text
95 /// after the `#` and a single space. Use [`set_text`](Self::set_text) when
96 /// you need full control over the marker and spacing.
97 ///
98 /// # Errors
99 ///
100 /// Returns [`CommentTextError::LineBreak`] if `content` contains a line
101 /// break, and [`CommentTextError::NonPrintable`] if it contains a
102 /// character YAML does not allow.
103 ///
104 /// # Example
105 /// ```
106 /// use yaml_edit::Document;
107 /// use std::str::FromStr;
108 ///
109 /// let doc = Document::from_str("key: value # teh value\n").unwrap();
110 /// let mut comment = doc.comments().next().unwrap();
111 /// comment.set_content("the value").unwrap();
112 /// assert_eq!(doc.to_string(), "key: value # the value\n");
113 /// ```
114 pub fn set_content(&mut self, content: &str) -> Result<(), CommentTextError> {
115 self.set_text(&format!("# {content}"))
116 }
117
118 /// Access the underlying syntax token.
119 pub fn syntax(&self) -> &SyntaxToken {
120 &self.0
121 }
122}
123
124/// Why [`Comment::set_text`] or [`Comment::set_content`] refused new text.
125#[derive(Debug, Clone, Copy, PartialEq, Eq)]
126pub enum CommentTextError {
127 /// The text does not start with `#`.
128 MissingHash,
129 /// The text contains a line break.
130 LineBreak,
131 /// The text contains a character YAML does not allow, such as a control
132 /// character or a byte order mark.
133 NonPrintable(char),
134}
135
136impl std::fmt::Display for CommentTextError {
137 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138 match self {
139 CommentTextError::MissingHash => write!(f, "comment text must start with '#'"),
140 CommentTextError::LineBreak => write!(f, "comment text must not contain a line break"),
141 CommentTextError::NonPrintable(c) => write!(
142 f,
143 "comment text must not contain the non-printable character U+{:04X}",
144 *c as u32
145 ),
146 }
147 }
148}
149
150impl std::error::Error for CommentTextError {}
151
152impl std::fmt::Debug for Comment {
153 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154 f.debug_struct("Comment")
155 .field("text", &self.text())
156 .finish()
157 }
158}
159
160impl std::fmt::Display for Comment {
161 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162 write!(f, "{}", self.0.text())
163 }
164}
165
166/// Iterate over every comment in `node` and its descendants, in source order.
167pub(crate) fn comments(node: &SyntaxNode) -> impl Iterator<Item = Comment> {
168 node.descendants_with_tokens()
169 .filter_map(|element| element.into_token())
170 .filter_map(Comment::cast)
171}