1use std::fmt;
4use std::ops::Deref;
5
6use facet::Facet;
7
8use crate::columns::Columns;
9use crate::error::{ColumnsError, TextError};
10use crate::header::GENERATED;
11
12#[derive(Debug, Clone, Copy, Facet, PartialEq, Eq)]
14#[repr(u8)]
15pub enum CommentSyntax {
16 #[facet(rename = "hash")]
18 Hash,
19 #[facet(rename = "double-slash")]
21 DoubleSlash,
22 #[facet(rename = "semicolon")]
24 Semicolon,
25 #[facet(rename = "xml")]
27 Xml,
28}
29
30impl CommentSyntax {
31 pub(crate) const ALL: [Self; 4] = [Self::Hash, Self::DoubleSlash, Self::Semicolon, Self::Xml];
33
34 pub(crate) fn uncomment(self, line: &str) -> Option<&str> {
36 match self.line_delimiter() {
37 Some(delimiter) => line.strip_prefix(delimiter),
38 None => line.strip_prefix("<!--")?.strip_suffix("-->"),
39 }
40 }
41
42 const fn line_delimiter(self) -> Option<&'static str> {
44 match self {
45 Self::Hash => Some("#"),
46 Self::DoubleSlash => Some("//"),
47 Self::Semicolon => Some(";"),
48 Self::Xml => None,
49 }
50 }
51
52 pub(crate) fn comment(self, comment: &str) -> impl fmt::Display + '_ {
57 Rendered {
58 syntax: self,
59 comment,
60 }
61 }
62
63 pub(crate) fn write_lines(
65 self,
66 out: &mut impl fmt::Write,
67 comment: &str,
68 indent: impl fmt::Display,
69 ) -> fmt::Result {
70 let Some(delimiter) = self.line_delimiter() else {
71 return if comment.is_empty() {
72 writeln!(out, "{indent}<!-- -->")
73 } else if comment.contains('\n') {
74 writeln!(out, "{indent}<!--")?;
75 comment
76 .split('\n')
77 .try_for_each(|line| writeln!(out, "{indent}{line}"))?;
78 writeln!(out, "{indent}-->")
79 } else {
80 writeln!(out, "{indent}<!-- {comment} -->")
81 };
82 };
83 comment.split('\n').try_for_each(|line| {
84 if line.is_empty() {
85 writeln!(out, "{indent}{delimiter}")
86 } else {
87 writeln!(out, "{indent}{delimiter} {line}")
88 }
89 })
90 }
91}
92
93struct Rendered<'a> {
94 syntax: CommentSyntax,
95 comment: &'a str,
96}
97
98impl fmt::Display for Rendered<'_> {
99 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100 self.syntax.write_lines(f, self.comment, "")
101 }
102}
103
104#[derive(Debug, Clone)]
106pub(crate) struct TextDocument<'a> {
107 parts: Vec<Part<'a>>,
108}
109
110#[derive(Debug, Clone)]
111enum Part<'a> {
112 Comment {
113 syntax: CommentSyntax,
114 comment: &'a str,
115 },
116 Text {
117 content: &'a str,
118 wrap: Option<Wrap>,
119 },
120}
121
122impl<'a> TextDocument<'a> {
123 pub(crate) fn new(
132 syntax: Option<CommentSyntax>,
133 nodes: &'a [DocumentNode],
134 ) -> Result<Self, TextError> {
135 let mut parts = Vec::with_capacity(nodes.len());
136 let mut at_line_start = true;
137 for node in nodes {
138 match node {
139 DocumentNode::Text { content, wrap } => {
140 if let Some(last) = content.chars().next_back() {
141 at_line_start = wrap.is_some() || last == '\n';
142 }
143 parts.push(Part::Text {
144 content,
145 wrap: *wrap,
146 });
147 }
148 DocumentNode::Comment { content } => {
149 let syntax = syntax.ok_or(TextError::CommentSyntaxMissing)?;
150 Comment::check(content)?;
151 let comment = content.as_str();
152 if syntax == CommentSyntax::Xml && comment.contains("-->") {
153 return Err(TextError::XmlCommentContainsCloser);
154 }
155 if !at_line_start {
156 return Err(TextError::CommentMidLine);
157 }
158 parts.push(Part::Comment { syntax, comment });
159 }
160 }
161 }
162 Ok(Self { parts })
163 }
164}
165
166impl TextDocument<'_> {
167 pub(crate) fn generated(mut self, syntax: Option<CommentSyntax>) -> Self {
170 if let Some(syntax) = syntax {
171 self.parts.splice(
172 ..0,
173 [
174 Part::Comment {
175 syntax,
176 comment: GENERATED,
177 },
178 Part::Text {
179 content: "\n",
180 wrap: None,
181 },
182 ],
183 );
184 }
185 self
186 }
187}
188
189impl fmt::Display for TextDocument<'_> {
190 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
191 for part in &self.parts {
192 match part {
193 Part::Comment { syntax, comment } => write!(f, "{}", syntax.comment(comment))?,
194 Part::Text {
195 content,
196 wrap: None,
197 } => f.write_str(content)?,
198 Part::Text {
199 content,
200 wrap: Some(width),
201 } => Wrapped {
202 content,
203 width: width.get(),
204 }
205 .fmt(f)?,
206 }
207 }
208 Ok(())
209 }
210}
211
212struct Wrapped<'a> {
214 content: &'a str,
215 width: usize,
216}
217
218impl Wrapped<'_> {
219 fn write_line(&self, line: &str, out: &mut impl fmt::Write) -> fmt::Result {
220 if line.chars().count() <= self.width {
221 return writeln!(out, "{line}");
222 }
223 let indent_len = line.find(|c| c != ' ' && c != '\t').unwrap_or(line.len());
224 let (indent, body) = line.split_at(indent_len);
225 let indent_width = indent.chars().count();
226 let mut column = 0;
227 for word in body.split_whitespace() {
228 let word_width = word.chars().count();
229 if column == 0 {
230 write!(out, "{indent}{word}")?;
231 column = indent_width.saturating_add(word_width);
232 } else if column.saturating_add(1).saturating_add(word_width) <= self.width {
233 write!(out, " {word}")?;
234 column = column.saturating_add(1).saturating_add(word_width);
235 } else {
236 write!(out, "\n{indent}{word}")?;
237 column = indent_width.saturating_add(word_width);
238 }
239 }
240 writeln!(out)
241 }
242}
243
244impl fmt::Display for Wrapped<'_> {
245 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
246 if self.content.is_empty() {
247 return Ok(());
248 }
249 let content = self.content.strip_suffix('\n').unwrap_or(self.content);
250 content
251 .split('\n')
252 .try_for_each(|line| self.write_line(line, f))
253 }
254}
255
256#[derive(Debug, Clone, PartialEq, Eq)]
260pub(crate) struct Comment(String);
261
262impl Comment {
263 pub(crate) fn new(text: String) -> Result<Self, TextError> {
270 Self::check(&text)?;
271 Ok(Self(text))
272 }
273
274 pub(crate) fn check(text: &str) -> Result<(), TextError> {
279 if text.contains('\r') {
280 return Err(TextError::CommentNotLf);
281 }
282 if let Some(character) = text.chars().find(|c| is_unsafe_text_char(*c)) {
283 return Err(TextError::CommentCharacter { character });
284 }
285 Ok(())
286 }
287}
288
289impl Deref for Comment {
290 type Target = str;
291
292 fn deref(&self) -> &str {
293 &self.0
294 }
295}
296
297#[derive(Debug, Clone, Copy, Facet, PartialEq, Eq)]
299#[facet(proxy = usize)]
300pub struct Wrap(#[facet(opaque)] Columns);
301
302impl Wrap {
303 #[must_use]
305 pub const fn new(columns: usize) -> Option<Self> {
306 match Columns::new(columns) {
307 Some(columns) => Some(Self(columns)),
308 None => None,
309 }
310 }
311
312 #[must_use]
314 pub const fn get(self) -> usize {
315 self.0.get()
316 }
317}
318
319impl TryFrom<usize> for Wrap {
320 type Error = ColumnsError;
321
322 fn try_from(columns: usize) -> Result<Self, Self::Error> {
323 Columns::try_from(columns).map(Self)
324 }
325}
326
327impl From<&Wrap> for usize {
328 fn from(wrap: &Wrap) -> Self {
329 wrap.get()
330 }
331}
332
333#[derive(Debug, Clone, Facet, PartialEq, Eq)]
335#[facet(tag = "type", deny_unknown_fields)]
336#[repr(u8)]
337pub enum DocumentNode {
338 #[facet(rename = "comment")]
340 Comment {
341 content: String,
343 },
344 #[facet(rename = "text")]
346 Text {
347 content: String,
349 #[facet(default)]
351 wrap: Option<Wrap>,
352 },
353}
354
355pub(crate) const fn is_unsafe_text_char(c: char) -> bool {
359 (c.is_control() && !matches!(c, '\n' | '\t')) || matches!(c, '\u{2028}' | '\u{2029}')
360}
361
362#[cfg(test)]
363mod tests;