Line data Source code
1 : //! Comment output of the formatter.
2 : //!
3 : //! Comments work on bytes: wrapping splits long words at byte offsets, which can split UTF-8
4 : //! sequences (see docs/design/core_quirks.md).
5 :
6 : use crate::annotation::{AnnotationTag, Annotation, ANNOTATION_GUTTER};
7 : use dashql_parser::comment_blocks::{
8 : group_comment_bytes, is_comment_tag, CommentBlockType, COMMENT_TAG_END, COMMENT_TAG_PREFIX,
9 : };
10 : use dashql_parser::span::TextSpan;
11 :
12 : /// `std::isspace` in the C locale
13 3268 : pub(super) fn is_space(ch: u8) -> bool {
14 3268 : matches!(ch, b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r')
15 3268 : }
16 :
17 344 : fn is_whitespace(text: &[u8]) -> bool {
18 368 : text.iter().all(|&ch| is_space(ch))
19 344 : }
20 :
21 1043 : fn trim_whitespace(mut text: &[u8]) -> &[u8] {
22 1873 : while let [first, rest @ ..] = text {
23 1826 : if !is_space(*first) {
24 996 : break;
25 830 : }
26 830 : text = rest;
27 : }
28 1045 : while let [rest @ .., last] = text {
29 998 : if !is_space(*last) {
30 996 : break;
31 2 : }
32 2 : text = rest;
33 : }
34 1043 : text
35 1043 : }
36 :
37 : /// `std::string_view::substr`, clamped to the text
38 2284 : fn substr(text: &[u8], offset: usize, length: usize) -> &[u8] {
39 2284 : let begin = offset.min(text.len());
40 2284 : let end = begin + length.min(text.len() - begin);
41 2284 : &text[begin..end]
42 2284 : }
43 :
44 1854 : fn comment_text<'a>(input: &'a [u8], comment: &TextSpan) -> &'a [u8] {
45 1854 : substr(input, comment.offset as usize, comment.length as usize)
46 1854 : }
47 :
48 : /// `find_first_of`
49 749 : fn find_first_of(text: &[u8], chars: &[u8]) -> Option<usize> {
50 5932 : text.iter().position(|ch| chars.contains(ch))
51 749 : }
52 :
53 149 : fn find_subslice(text: &[u8], needle: &[u8]) -> Option<usize> {
54 3516 : text.windows(needle.len()).position(|w| w == needle)
55 149 : }
56 :
57 425 : fn count_line_breaks(text: &[u8]) -> usize {
58 448 : text.iter().filter(|&&ch| ch == b'\n').count()
59 425 : }
60 :
61 162 : fn ends_with_line_break(output: &[u8]) -> bool {
62 162 : output.last() == Some(&b'\n')
63 162 : }
64 :
65 160 : fn append_comment_line(output: &mut Vec<u8>, text: &[u8], max_width: usize) {
66 160 : if !output.is_empty() && !ends_with_line_break(output) {
67 92 : output.push(b'\n');
68 98 : }
69 160 : output.extend_from_slice(b"--");
70 160 : if text.is_empty() {
71 7 : return;
72 153 : }
73 153 : if max_width >= 4 {
74 153 : output.push(b' ');
75 153 : }
76 153 : output.extend_from_slice(text);
77 160 : }
78 :
79 103 : fn append_wrapped_comment_paragraph(output: &mut Vec<u8>, words: &[&[u8]], max_width: usize) {
80 103 : if words.is_empty() {
81 0 : append_comment_line(output, &[], max_width);
82 0 : return;
83 103 : }
84 :
85 103 : let prefix_width = if max_width >= 4 { 3 } else { 2 };
86 103 : let content_width = if max_width > prefix_width { max_width - prefix_width } else { 1 };
87 103 : let mut line: Vec<u8> = Vec::new();
88 393 : for word in words {
89 393 : let mut offset = 0;
90 836 : while offset < word.len() {
91 443 : let remaining = word.len() - offset;
92 443 : if !line.is_empty() && line.len() + 1 + remaining > content_width {
93 45 : append_comment_line(output, &line, max_width);
94 45 : line.clear();
95 45 : continue;
96 398 : }
97 :
98 398 : let separator = if line.is_empty() { 0 } else { 1 };
99 398 : if remaining <= content_width - line.len() - separator {
100 393 : if !line.is_empty() {
101 245 : line.push(b' ');
102 258 : }
103 393 : line.extend_from_slice(&word[offset..]);
104 393 : offset = word.len();
105 : } else {
106 5 : let available = content_width - line.len() - separator;
107 5 : if !line.is_empty() {
108 0 : line.push(b' ');
109 5 : }
110 5 : line.extend_from_slice(&word[offset..offset + available]);
111 5 : offset += available;
112 5 : append_comment_line(output, &line, max_width);
113 5 : line.clear();
114 : }
115 : }
116 : }
117 103 : if !line.is_empty() {
118 103 : append_comment_line(output, &line, max_width);
119 103 : }
120 103 : }
121 :
122 100 : fn append_wrapped_line_comments(output: &mut Vec<u8>, input: &[u8], comments: &[TextSpan], max_width: usize) {
123 100 : let mut normalized_lines: Vec<&[u8]> = Vec::new();
124 143 : for comment in comments {
125 143 : let mut text = comment_text(input, comment);
126 143 : if text.starts_with(b"--") {
127 143 : text = &text[2..];
128 143 : }
129 : loop {
130 143 : let line_end = find_first_of(text, b"\r\n");
131 143 : let line = trim_whitespace(&text[..line_end.unwrap_or(text.len())]);
132 143 : normalized_lines.push(line);
133 :
134 143 : let Some(line_end) = line_end else { break };
135 0 : let mut next = line_end + 1;
136 0 : if text[line_end] == b'\r' && next < text.len() && text[next] == b'\n' {
137 0 : next += 1;
138 0 : }
139 0 : text = &text[next..];
140 : }
141 : }
142 :
143 104 : let first = normalized_lines.iter().position(|l| !l.is_empty()).unwrap_or(normalized_lines.len());
144 100 : normalized_lines.drain(..first);
145 104 : while normalized_lines.last().is_some_and(|l| l.is_empty()) {
146 4 : normalized_lines.pop();
147 4 : }
148 :
149 100 : if normalized_lines.is_empty() {
150 2 : append_comment_line(output, &[], max_width);
151 2 : return;
152 98 : }
153 :
154 98 : let mut words: Vec<&[u8]> = Vec::new();
155 133 : for line in normalized_lines {
156 133 : if line.is_empty() {
157 5 : if !words.is_empty() {
158 5 : append_wrapped_comment_paragraph(output, &words, max_width);
159 5 : words.clear();
160 5 : }
161 5 : append_comment_line(output, &[], max_width);
162 5 : continue;
163 128 : }
164 :
165 128 : let mut remaining = line;
166 393 : while !remaining.is_empty() {
167 393 : let word_end = find_first_of(remaining, b" \t\r\n\x0c");
168 393 : let word = &remaining[..word_end.unwrap_or(remaining.len())];
169 393 : if !word.is_empty() {
170 393 : words.push(word);
171 393 : }
172 393 : let Some(word_end) = word_end else { break };
173 265 : remaining = trim_whitespace(&remaining[word_end + 1..]);
174 : }
175 : }
176 98 : if !words.is_empty() {
177 98 : append_wrapped_comment_paragraph(output, &words, max_width);
178 98 : }
179 100 : }
180 :
181 : /// The tag name of a `-- @<tag>` line comment and whether the tag is alone on its line
182 : struct CommentTag<'a> {
183 : /// The tag name after `@`
184 : name: &'a [u8],
185 : /// Is the tag the only text after the comment marker?
186 : alone: bool,
187 : }
188 :
189 566 : fn read_comment_tag<'a>(input: &'a [u8], comment: &TextSpan) -> Option<CommentTag<'a>> {
190 566 : let text = comment_text(input, comment);
191 566 : let text = trim_whitespace(text.strip_prefix(b"--")?);
192 566 : let text = text.strip_prefix(COMMENT_TAG_PREFIX.as_bytes())?;
193 213 : let name_end = find_first_of(text, b" \t\r\n\x0c");
194 213 : let name = &text[..name_end.unwrap_or(text.len())];
195 213 : if !is_comment_tag(name) {
196 28 : return None;
197 185 : }
198 185 : let alone = match name_end {
199 117 : None => true,
200 68 : Some(end) => trim_whitespace(&text[end..]).is_empty(),
201 : };
202 185 : Some(CommentTag { name, alone })
203 566 : }
204 :
205 : /// The tag of visualizations, `@vis <kind> ...`
206 : const VIS_TAG: &str = "vis";
207 : /// The tag of the dashboard placement, `@board <args>`
208 : const BOARD_TAG: &str = "board";
209 : /// The tag of the position on the dataflow canvas, `@flow <args>`
210 : const FLOW_TAG: &str = "flow";
211 :
212 : /// Split a line at its first whitespace, the rest is trimmed
213 92 : fn split_word(line: &str) -> (&str, &str) {
214 92 : let end = line.find([' ', '\t', '\r', '\x0c', '\x0b']).unwrap_or(line.len());
215 92 : (&line[..end], line[end..].trim())
216 92 : }
217 :
218 : /// The canonical content lines of a `@vis` tag, the first line is the text on the line of the tag.
219 : /// `max_width` is the width of the comment lines.
220 82 : fn vis_tag_lines(lines: &[String], max_width: usize) -> Vec<String> {
221 82 : let Some((header, body)) = lines.split_first() else { return Vec::new() };
222 82 : let (kind, rest) = split_word(header);
223 82 : match kind {
224 82 : "vegalite" => {
225 : // The JSON is printed compactly with the key order kept, malformed JSON stays as written
226 51 : let json = std::iter::once(rest).chain(body.iter().map(String::as_str)).collect::<Vec<_>>().join("\n");
227 51 : let Ok(value) = serde_json::from_str::<serde_json::Value>(&json) else { return lines.to_vec() };
228 41 : let width = max_width.saturating_sub(ANNOTATION_GUTTER.len());
229 41 : let compact = crate::compact_json::to_string_compact(&value, width);
230 41 : std::iter::once(kind.to_string()).chain(compact.lines().map(str::to_string)).collect()
231 : }
232 31 : "table" => {
233 : // Args are separated by single spaces
234 17 : let header = std::iter::once(kind).chain(rest.split_whitespace()).collect::<Vec<_>>().join(" ");
235 17 : std::iter::once(header).chain(body.iter().cloned()).collect()
236 : }
237 14 : _ => lines.to_vec(),
238 : }
239 82 : }
240 :
241 : /// The canonical content lines of a `@board` or `@flow` tag: its args, separated by single spaces
242 21 : fn args_tag_lines(lines: &[String]) -> Vec<String> {
243 21 : let Some((header, body)) = lines.split_first() else { return Vec::new() };
244 21 : std::iter::once(header.split_whitespace().collect::<Vec<_>>().join(" ")).chain(body.iter().cloned()).collect()
245 21 : }
246 :
247 : /// The `@board` or `@flow` tag of the `@vis board`, `@vis card` or `@vis flow` of an older script, with its args
248 177 : fn legacy_layout_tag(tag: &AnnotationTag) -> Option<AnnotationTag> {
249 177 : if tag.name != VIS_TAG {
250 85 : return None;
251 92 : }
252 92 : let name = match tag.kind() {
253 92 : "board" | "card" => BOARD_TAG,
254 83 : "flow" => FLOW_TAG,
255 82 : _ => return None,
256 : };
257 10 : let (_, args) = split_word(tag.lines.first().map(String::as_str).unwrap_or("").trim_start());
258 10 : let lines = std::iter::once(args.to_string()).chain(tag.lines.iter().skip(1).cloned()).collect();
259 10 : Some(AnnotationTag::new(name, lines))
260 177 : }
261 :
262 : /// Print comments that hold tags as one canonical tagged comment, like an annotation (see `annotation`):
263 : /// the tags keep their order, `@vis`, `@board` and `@flow` tags are normalized and the `@vis board` and `@vis flow` of
264 : /// older scripts become `@board` and `@flow`, the ordinary comments and the text before the first tag of a block
265 : /// comment go in front of them. Returns None if the comments stay as written: without a tag, with malformed tags,
266 : /// or with text that would end or nest a block comment.
267 123 : fn print_tagged_comment(input: &[u8], comments: &[TextSpan], max_width: usize) -> Option<Vec<u8>> {
268 123 : let blocks = group_comment_bytes(input, comments);
269 132 : if !blocks.warnings.is_empty() || !blocks.blocks.iter().any(|b| b.block_type == CommentBlockType::DASHQL_TAG) {
270 2 : return None;
271 121 : }
272 121 : let mut annotation = Annotation::from_blocks(input, &blocks, blocks.blocks.iter());
273 177 : for tag in annotation.tags.iter_mut() {
274 : // Older scripts place visualizations with `@vis board` and scripts with `@vis flow`
275 177 : if let Some(layout) = legacy_layout_tag(tag) {
276 10 : *tag = layout;
277 167 : }
278 177 : match tag.name.as_str() {
279 177 : VIS_TAG => tag.lines = vis_tag_lines(&tag.lines, max_width),
280 95 : BOARD_TAG | FLOW_TAG => tag.lines = args_tag_lines(&tag.lines),
281 74 : _ => {}
282 : }
283 : }
284 121 : annotation.print("").ok().map(|(comment, _)| comment.into_bytes())
285 123 : }
286 :
287 : /// Is a comment a block comment?
288 920 : fn is_block_comment(input: &[u8], comment: &TextSpan) -> bool {
289 920 : comment_text(input, comment).starts_with(b"/*")
290 920 : }
291 :
292 : /// Does a block comment hold a tag?
293 149 : fn block_comment_has_tag(input: &[u8], comment: TextSpan) -> bool {
294 149 : find_subslice(comment_text(input, &comment), COMMENT_TAG_PREFIX.as_bytes()).is_some()
295 70 : && group_comment_bytes(input, &[comment]).blocks.iter().any(|b| b.block_type == CommentBlockType::DASHQL_TAG)
296 149 : }
297 :
298 : /// The text between two comments
299 343 : fn gap_between<'a>(input: &'a [u8], previous: &TextSpan, next: &TextSpan) -> &'a [u8] {
300 343 : let end = previous.end() as usize;
301 343 : substr(input, end, (next.offset as usize).saturating_sub(end))
302 343 : }
303 :
304 : /// A slice of the comments: ordinary comments, or comments with tags
305 : struct Unit {
306 : /// The comments of the unit
307 : comments: std::ops::Range<usize>,
308 : /// Does the unit hold tags?
309 : tagged: bool,
310 : }
311 :
312 : /// Split comments into units. A block comment is a unit. A run of line comments on consecutive lines is
313 : /// split into tagged regions, from a `-- @<tag>` line to the end of the run or to an `-- @end` line that
314 : /// older scripts close it with, and the ordinary comments between them.
315 11117 : fn comment_units(input: &[u8], comments: &[TextSpan]) -> Vec<Unit> {
316 11117 : let opens = |c: &TextSpan| read_comment_tag(input, c).is_some_and(|t| t.name != COMMENT_TAG_END.as_bytes());
317 11117 : let closes = |c: &TextSpan| read_comment_tag(input, c).is_some_and(|t| t.name == COMMENT_TAG_END.as_bytes() && t.alone);
318 11117 : let mut units = Vec::new();
319 11117 : let mut i = 0;
320 11512 : while i < comments.len() {
321 395 : if is_block_comment(input, &comments[i]) {
322 149 : units.push(Unit { comments: i..i + 1, tagged: block_comment_has_tag(input, comments[i]) });
323 149 : i += 1;
324 149 : continue;
325 246 : }
326 246 : let mut run_end = i + 1;
327 566 : while run_end < comments.len() && !is_block_comment(input, &comments[run_end]) {
328 340 : let gap = gap_between(input, &comments[run_end - 1], &comments[run_end]);
329 340 : if !is_whitespace(gap) || count_line_breaks(gap) > 1 {
330 20 : break;
331 320 : }
332 320 : run_end += 1;
333 : }
334 246 : let mut pending = i;
335 246 : let mut j = i;
336 588 : while j < run_end {
337 342 : if !opens(&comments[j]) {
338 258 : j += 1;
339 258 : continue;
340 84 : }
341 84 : let mut close = j + 1;
342 237 : while close < run_end && !closes(&comments[close]) {
343 153 : close += 1;
344 153 : }
345 84 : let end = (close + 1).min(run_end);
346 84 : if pending < j {
347 6 : units.push(Unit { comments: pending..j, tagged: false });
348 78 : }
349 84 : units.push(Unit { comments: j..end, tagged: true });
350 84 : pending = end;
351 84 : j = end;
352 : }
353 246 : if pending < run_end {
354 171 : units.push(Unit { comments: pending..run_end, tagged: false });
355 171 : }
356 246 : i = run_end;
357 : }
358 11117 : units
359 11117 : }
360 :
361 : /// Append comments as written, without trailing whitespace
362 64 : fn append_verbatim(output: &mut Vec<u8>, input: &[u8], comments: &[TextSpan]) {
363 76 : for comment in comments {
364 76 : if !output.is_empty() && !ends_with_line_break(output) {
365 45 : output.push(b'\n');
366 45 : }
367 76 : let mut text = comment_text(input, comment);
368 76 : while let [rest @ .., last] = text {
369 76 : if !is_space(*last) {
370 76 : break;
371 0 : }
372 0 : text = rest;
373 : }
374 76 : output.extend_from_slice(text);
375 : }
376 64 : }
377 :
378 : /// Append comments: block comments verbatim, runs of line comments rewrapped. Comments with tags that
379 : /// follow each other with nothing but whitespace between them are merged into one canonical tagged
380 : /// comment, so an annotation reads as one block. Comments with tags that cannot be printed canonically
381 : /// are kept as written, since their content is structured (e.g. JSON) and rewrapping it would break it.
382 : /// A blank line that follows tagged comments is kept, `following` is the offset of the text after the
383 : /// comments.
384 10956 : pub(super) fn append_comments(
385 10956 : output: &mut Vec<u8>,
386 10956 : input: &[u8],
387 10956 : comments: &[TextSpan],
388 10956 : max_width: usize,
389 10956 : following: Option<usize>,
390 10956 : ) {
391 10956 : let units = comment_units(input, comments);
392 10956 : let mut u = 0;
393 11148 : while u < units.len() {
394 192 : let unit = &units[u];
395 192 : if !unit.tagged {
396 158 : let slice = &comments[unit.comments.clone()];
397 158 : if is_block_comment(input, &slice[0]) {
398 58 : append_verbatim(output, input, slice);
399 102 : } else {
400 100 : append_wrapped_line_comments(output, input, slice, max_width);
401 100 : }
402 158 : u += 1;
403 158 : continue;
404 34 : }
405 : // The tagged units that follow after whitespace only
406 34 : let mut last = u;
407 37 : while last + 1 < units.len()
408 8 : && units[last + 1].tagged
409 3 : && is_whitespace(gap_between(input, &comments[units[last].comments.end - 1], &comments[units[last + 1].comments.start]))
410 3 : {
411 3 : last += 1;
412 3 : }
413 34 : let range = unit.comments.start..units[last].comments.end;
414 34 : if !output.is_empty() && !ends_with_line_break(output) {
415 1 : output.push(b'\n');
416 33 : }
417 34 : match print_tagged_comment(input, &comments[range.clone()], max_width) {
418 28 : Some(block) => output.extend_from_slice(&block),
419 : // Units that cannot be merged are printed one by one
420 : None => {
421 7 : for unit in &units[u..=last] {
422 7 : let slice = &comments[unit.comments.clone()];
423 7 : if !output.is_empty() && !ends_with_line_break(output) {
424 1 : output.push(b'\n');
425 6 : }
426 7 : match print_tagged_comment(input, slice, max_width) {
427 1 : Some(block) => output.extend_from_slice(&block),
428 6 : None => append_verbatim(output, input, slice),
429 : }
430 : }
431 : }
432 : }
433 34 : let next = comments.get(range.end).map(|c| c.offset as usize).or(following);
434 34 : keep_blank_line(output, input, comments[range.end - 1].end() as usize, next);
435 34 : u = last + 1;
436 : }
437 10956 : }
438 :
439 : /// Keep a blank line between the end of tagged comments and the text that follows them at `next`
440 110 : fn keep_blank_line(output: &mut Vec<u8>, input: &[u8], text_end: usize, next: Option<usize>) {
441 110 : if let Some(next) = next.filter(|&next| next >= text_end) {
442 85 : if count_line_breaks(substr(input, text_end, next - text_end)) > 1 {
443 3 : output.extend_from_slice(b"\n\n");
444 82 : }
445 25 : }
446 110 : }
447 :
448 : /// Append the comments in front of a statement. With a tag among them they are its annotation: one canonical
449 : /// comment with the ordinary comments as the description on top, like a rewrite of a tag prints it (see
450 : /// `annotation`). Otherwise, or if the annotation cannot be printed canonically, they are appended like other
451 : /// comments. `following` is the offset of the statement.
452 161 : pub(super) fn append_description(output: &mut Vec<u8>, input: &[u8], comments: &[TextSpan], max_width: usize, following: Option<usize>) {
453 161 : let annotation = comment_units(input, comments)
454 161 : .iter()
455 161 : .any(|unit| unit.tagged)
456 161 : .then(|| print_tagged_comment(input, comments, max_width))
457 161 : .flatten();
458 161 : let Some(block) = annotation else {
459 85 : append_comments(output, input, comments, max_width, following);
460 85 : return;
461 : };
462 76 : if !output.is_empty() && !ends_with_line_break(output) {
463 0 : output.push(b'\n');
464 76 : }
465 76 : output.extend_from_slice(&block);
466 76 : keep_blank_line(output, input, comments[comments.len() - 1].end() as usize, following);
467 161 : }
468 :
469 : #[cfg(test)]
470 : mod tests {
471 : use super::*;
472 :
473 8 : fn spans(input: &str, comments: &[&str]) -> Vec<TextSpan> {
474 8 : let mut from = 0;
475 8 : comments
476 8 : .iter()
477 20 : .map(|c| {
478 20 : let offset = from + input[from..].find(c).unwrap();
479 20 : from = offset + c.len();
480 20 : TextSpan::new(offset as u32, c.len() as u32)
481 20 : })
482 8 : .collect()
483 8 : }
484 :
485 8 : fn format(input: &str, comments: &[&str], max_width: usize) -> String {
486 8 : let mut out = Vec::new();
487 8 : append_comments(&mut out, input.as_bytes(), &spans(input, comments), max_width, None);
488 8 : String::from_utf8(out).unwrap()
489 8 : }
490 :
491 : #[test]
492 1 : fn rewraps_line_comments() {
493 1 : let input = "-- aaa bbb\n-- ccc";
494 1 : assert_eq!(format(input, &["-- aaa bbb", "-- ccc"], 120), "-- aaa bbb ccc");
495 1 : assert_eq!(format(input, &["-- aaa bbb", "-- ccc"], 10), "-- aaa bbb\n-- ccc");
496 1 : assert_eq!(format(input, &["-- aaa bbb", "-- ccc"], 9), "-- aaa\n-- bbb\n-- ccc");
497 : // Words longer than the line are split
498 1 : assert_eq!(format("--abcdefgh", &["--abcdefgh"], 6), "-- abc\n-- def\n-- gh");
499 1 : }
500 :
501 : #[test]
502 1 : fn keeps_blank_lines_and_block_comments() {
503 1 : let input = "-- a\n\n-- b\n/* c */";
504 1 : assert_eq!(format(input, &["-- a", "-- b", "/* c */"], 120), "-- a\n-- b\n/* c */");
505 1 : let input = "-- a\n--\n-- b";
506 1 : assert_eq!(format(input, &["-- a", "--", "-- b"], 120), "-- a\n--\n-- b");
507 1 : }
508 :
509 : #[test]
510 1 : fn prints_tagged_regions() {
511 : // A tagged region ends with the run or with an end line, comments after it are rewrapped
512 1 : let input = "-- @vis markdown\n-- **{{a}}** x\n-- @end\n\n-- x y";
513 1 : let out = format(input, &["-- @vis markdown", "-- **{{a}}** x", "-- @end", "-- x y"], 120);
514 1 : assert_eq!(out, "/**\n * @vis markdown\n * **{{a}}** x\n */\n\n-- x y");
515 1 : let input = "-- @param Sales.\n-- @end\n-- x y";
516 1 : assert_eq!(format(input, &["-- @param Sales.", "-- @end", "-- x y"], 120), "/** @param Sales. */\n-- x y");
517 1 : }
518 :
519 : /// Format a script that consists of comments, every comment is found by the scanner
520 25 : fn format_comments(input: &str) -> String {
521 25 : format_comments_within(input, 120)
522 25 : }
523 :
524 26 : fn format_comments_within(input: &str, max_width: usize) -> String {
525 26 : let scanned = dashql_parser::scanner::scan(input, 0, 0);
526 26 : let mut out = Vec::new();
527 26 : append_comments(&mut out, input.as_bytes(), &scanned.comments, max_width, None);
528 26 : String::from_utf8(out).unwrap()
529 26 : }
530 :
531 : #[test]
532 1 : fn prints_vis_block_comments() {
533 1 : assert_eq!(format_comments("/* @board width=6 height=3 */"), "/** @board width=6 height=3 */");
534 1 : assert_eq!(format_comments("/* @flow x=1 y=2 */"), "/** @flow x=1 y=2 */");
535 : // The placements of older scripts become their own tags
536 1 : assert_eq!(format_comments("/* @vis board width=6 height=3 */"), "/** @board width=6 height=3 */");
537 1 : assert_eq!(format_comments("/* @vis card width=6 */"), "/** @board width=6 */");
538 1 : assert_eq!(format_comments("/* @vis table\n @vis flow x=1 y=2 */"), "/**\n * @vis table\n * @flow x=1 y=2\n */");
539 1 : assert_eq!(format_comments("/* @vis table */"), "/** @vis table */");
540 1 : let input = "/* @vis vegalite {\"mark\":\"bar\", \"encoding\": {\"x\": {\"field\": \"a\"}}} */";
541 1 : assert_eq!(format_comments(input), "/**\n * @vis vegalite\n * {\"mark\": \"bar\", \"encoding\": {\"x\": {\"field\": \"a\"}}}\n */");
542 : // The JSON breaks where it does not fit in the comment lines
543 1 : assert_eq!(
544 1 : format_comments_within(input, 40),
545 : "/**\n * @vis vegalite\n * {\n * \"mark\": \"bar\",\n * \"encoding\": {\"x\": {\"field\": \"a\"}}\n * }\n */"
546 : );
547 : // Formatting twice gives the same output
548 1 : let once = format_comments("/* @vis vegalite\n{\"mark\":\"bar\"}\n*/");
549 1 : assert_eq!(format_comments(&once), once);
550 1 : }
551 :
552 : #[test]
553 1 : fn prints_tagged_line_comments_as_block_comment() {
554 1 : let expected = "/**\n * @param Revenue.\n * @board width=6\n * @vis vegalite\n * {\"mark\": \"bar\"}\n */";
555 3 : for input in [
556 1 : "-- @param Revenue.\n-- @vis board width=6\n-- @vis vegalite\n-- {\"mark\": \"bar\"}",
557 1 : "-- @param Revenue.\n-- @vis board width=6\n-- @vis vegalite\n-- {\"mark\": \"bar\"}\n-- @end",
558 1 : "/* @param Revenue.\n @vis board width=6\n @vis vegalite\n {\"mark\": \"bar\"}\n*/",
559 1 : ] {
560 3 : assert_eq!(format_comments(input), expected, "{input}");
561 : }
562 1 : assert_eq!(format_comments(expected), expected);
563 : // Descriptions keep their lines, an empty line keeps its gutter
564 1 : let input = "-- @param\n-- Revenue.\n--\n-- * per region\n-- @end";
565 1 : assert_eq!(format_comments(input), "/**\n * @param\n * Revenue.\n *\n * * per region\n */");
566 1 : assert_eq!(format_comments("/* @param Revenue. */"), "/** @param Revenue. */");
567 1 : }
568 :
569 : #[test]
570 1 : fn merges_tagged_comments() {
571 1 : let input = "-- @param\n-- Add to timestamps.\n-- @end\n\n/* @vis vegalite\n {\"mark\": \"bar\"}\n*/";
572 1 : assert_eq!(format_comments(input), "/**\n * @param\n * Add to timestamps.\n * @vis vegalite\n * {\"mark\": \"bar\"}\n */");
573 : // Ordinary comments between them keep them apart
574 1 : let input = "/* @param Sales. */\n-- note\n/* @vis table */";
575 1 : assert_eq!(format_comments(input), "/** @param Sales. */\n-- note\n/** @vis table */");
576 : // The text before the first tag of a block comment goes in front of all tags
577 1 : let input = "/* @param Sales. */\n/* Note.\n @vis table */";
578 1 : assert_eq!(format_comments(input), "/**\n * Note.\n *\n * @param Sales.\n * @vis table\n */");
579 : // Comments that cannot be printed canonically stay apart
580 1 : let input = "/* @param Sales. */\n-- @vis markdown\n-- see */ here";
581 1 : assert_eq!(format_comments(input), "/** @param Sales. */\n-- @vis markdown\n-- see */ here");
582 1 : }
583 :
584 : #[test]
585 1 : fn keeps_text_before_the_first_tag() {
586 1 : let input = "/* Revenue dashboard.\n @vis table\n*/";
587 1 : assert_eq!(format_comments(input), "/**\n * Revenue dashboard.\n *\n * @vis table\n */");
588 : // Comments without tags stay as written
589 1 : let input = "/* Revenue dashboard.\n @see x\n*/";
590 1 : assert_eq!(format_comments(input), input);
591 1 : }
592 :
593 : #[test]
594 1 : fn keeps_markdown_and_malformed_json() {
595 1 : let input = "/**\n * @vis markdown\n * - {{a}}\n * - {{b}}\n */";
596 1 : assert_eq!(format_comments(input), input);
597 1 : let input = "/* @vis vegalite {\"mark\": */";
598 1 : assert_eq!(format_comments(input), "/** @vis vegalite {\"mark\": */");
599 1 : let input = "/* @vis vegalite\n {\"mark\":\n */";
600 1 : assert_eq!(format_comments(input), "/**\n * @vis vegalite\n * {\"mark\":\n */");
601 1 : }
602 :
603 : #[test]
604 1 : fn keeps_tagged_line_comments_that_cannot_be_block_comments() {
605 1 : let input = "-- @vis markdown\n-- see */ here";
606 1 : assert_eq!(format_comments(input), input);
607 1 : }
608 :
609 : #[test]
610 1 : fn trims_like_isspace() {
611 1 : assert_eq!(trim_whitespace(b"\x0b a \x0c"), b"a");
612 1 : assert!(is_whitespace(b" \t\r\n"));
613 1 : assert_eq!(substr(b"abc", 5, 2), b"");
614 1 : assert_eq!(substr(b"abc", 1, 10), b"bc");
615 1 : }
616 : }
|