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 3292 : pub(super) fn is_space(ch: u8) -> bool {
14 3292 : matches!(ch, b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r')
15 3292 : }
16 :
17 344 : fn is_whitespace(text: &[u8]) -> bool {
18 368 : text.iter().all(|&ch| is_space(ch))
19 344 : }
20 :
21 1049 : fn trim_whitespace(mut text: &[u8]) -> &[u8] {
22 1885 : while let [first, rest @ ..] = text {
23 1838 : if !is_space(*first) {
24 1002 : break;
25 836 : }
26 836 : text = rest;
27 : }
28 1051 : while let [rest @ .., last] = text {
29 1004 : if !is_space(*last) {
30 1002 : break;
31 2 : }
32 2 : text = rest;
33 : }
34 1049 : text
35 1049 : }
36 :
37 : /// `std::string_view::substr`, clamped to the text
38 2427 : fn substr(text: &[u8], offset: usize, length: usize) -> &[u8] {
39 2427 : let begin = offset.min(text.len());
40 2427 : let end = begin + length.min(text.len() - begin);
41 2427 : &text[begin..end]
42 2427 : }
43 :
44 1902 : fn comment_text<'a>(input: &'a [u8], comment: &TextSpan) -> &'a [u8] {
45 1902 : substr(input, comment.offset as usize, comment.length as usize)
46 1902 : }
47 :
48 : /// `find_first_of`
49 753 : fn find_first_of(text: &[u8], chars: &[u8]) -> Option<usize> {
50 5970 : text.iter().position(|ch| chars.contains(ch))
51 753 : }
52 :
53 161 : fn find_subslice(text: &[u8], needle: &[u8]) -> Option<usize> {
54 3740 : text.windows(needle.len()).position(|w| w == needle)
55 161 : }
56 :
57 520 : fn count_line_breaks(text: &[u8]) -> usize {
58 553 : text.iter().filter(|&&ch| ch == b'\n').count()
59 520 : }
60 :
61 164 : fn ends_with_line_break(output: &[u8]) -> bool {
62 164 : output.last() == Some(&b'\n')
63 164 : }
64 :
65 162 : fn append_comment_line(output: &mut Vec<u8>, text: &[u8], max_width: usize) {
66 162 : if !output.is_empty() && !ends_with_line_break(output) {
67 92 : output.push(b'\n');
68 98 : }
69 162 : output.extend_from_slice(b"--");
70 162 : if text.is_empty() {
71 7 : return;
72 155 : }
73 155 : if max_width >= 4 {
74 155 : output.push(b' ');
75 155 : }
76 155 : output.extend_from_slice(text);
77 162 : }
78 :
79 105 : fn append_wrapped_comment_paragraph(output: &mut Vec<u8>, words: &[&[u8]], max_width: usize) {
80 105 : if words.is_empty() {
81 0 : append_comment_line(output, &[], max_width);
82 0 : return;
83 105 : }
84 :
85 105 : let prefix_width = if max_width >= 4 { 3 } else { 2 };
86 105 : let content_width = if max_width > prefix_width { max_width - prefix_width } else { 1 };
87 105 : let mut line: Vec<u8> = Vec::new();
88 395 : for word in words {
89 395 : let mut offset = 0;
90 840 : while offset < word.len() {
91 445 : let remaining = word.len() - offset;
92 445 : 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 400 : }
97 :
98 400 : let separator = if line.is_empty() { 0 } else { 1 };
99 400 : if remaining <= content_width - line.len() - separator {
100 395 : if !line.is_empty() {
101 245 : line.push(b' ');
102 258 : }
103 395 : line.extend_from_slice(&word[offset..]);
104 395 : 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 105 : if !line.is_empty() {
118 105 : append_comment_line(output, &line, max_width);
119 105 : }
120 105 : }
121 :
122 102 : fn append_wrapped_line_comments(output: &mut Vec<u8>, input: &[u8], comments: &[TextSpan], max_width: usize) {
123 102 : let mut normalized_lines: Vec<&[u8]> = Vec::new();
124 145 : for comment in comments {
125 145 : let mut text = comment_text(input, comment);
126 145 : if text.starts_with(b"--") {
127 145 : text = &text[2..];
128 145 : }
129 : loop {
130 145 : let line_end = find_first_of(text, b"\r\n");
131 145 : let line = trim_whitespace(&text[..line_end.unwrap_or(text.len())]);
132 145 : normalized_lines.push(line);
133 :
134 145 : 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 106 : let first = normalized_lines.iter().position(|l| !l.is_empty()).unwrap_or(normalized_lines.len());
144 102 : normalized_lines.drain(..first);
145 106 : while normalized_lines.last().is_some_and(|l| l.is_empty()) {
146 4 : normalized_lines.pop();
147 4 : }
148 :
149 102 : if normalized_lines.is_empty() {
150 2 : append_comment_line(output, &[], max_width);
151 2 : return;
152 100 : }
153 :
154 100 : let mut words: Vec<&[u8]> = Vec::new();
155 135 : for line in normalized_lines {
156 135 : 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 130 : }
164 :
165 130 : let mut remaining = line;
166 395 : while !remaining.is_empty() {
167 395 : let word_end = find_first_of(remaining, b" \t\r\n\x0c");
168 395 : let word = &remaining[..word_end.unwrap_or(remaining.len())];
169 395 : if !word.is_empty() {
170 395 : words.push(word);
171 395 : }
172 395 : let Some(word_end) = word_end else { break };
173 265 : remaining = trim_whitespace(&remaining[word_end + 1..]);
174 : }
175 : }
176 100 : if !words.is_empty() {
177 100 : append_wrapped_comment_paragraph(output, &words, max_width);
178 100 : }
179 102 : }
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 570 : fn read_comment_tag<'a>(input: &'a [u8], comment: &TextSpan) -> Option<CommentTag<'a>> {
190 570 : let text = comment_text(input, comment);
191 570 : let text = trim_whitespace(text.strip_prefix(b"--")?);
192 570 : 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 570 : }
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 944 : fn is_block_comment(input: &[u8], comment: &TextSpan) -> bool {
289 944 : comment_text(input, comment).starts_with(b"/*")
290 944 : }
291 :
292 : /// Does a block comment hold a tag?
293 161 : fn block_comment_has_tag(input: &[u8], comment: TextSpan) -> bool {
294 161 : 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 161 : }
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 11781 : fn comment_units(input: &[u8], comments: &[TextSpan]) -> Vec<Unit> {
316 11781 : let opens = |c: &TextSpan| read_comment_tag(input, c).is_some_and(|t| t.name != COMMENT_TAG_END.as_bytes());
317 11781 : let closes = |c: &TextSpan| read_comment_tag(input, c).is_some_and(|t| t.name == COMMENT_TAG_END.as_bytes() && t.alone);
318 11781 : let mut units = Vec::new();
319 11781 : let mut i = 0;
320 12192 : while i < comments.len() {
321 411 : if is_block_comment(input, &comments[i]) {
322 161 : units.push(Unit { comments: i..i + 1, tagged: block_comment_has_tag(input, comments[i]) });
323 161 : i += 1;
324 161 : continue;
325 250 : }
326 250 : let mut run_end = i + 1;
327 570 : 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 250 : let mut pending = i;
335 250 : let mut j = i;
336 596 : while j < run_end {
337 346 : if !opens(&comments[j]) {
338 262 : j += 1;
339 262 : 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 250 : if pending < run_end {
354 175 : units.push(Unit { comments: pending..run_end, tagged: false });
355 175 : }
356 250 : i = run_end;
357 : }
358 11781 : units
359 11781 : }
360 :
361 : /// Append comments as written, without trailing whitespace
362 70 : fn append_verbatim(output: &mut Vec<u8>, input: &[u8], comments: &[TextSpan]) {
363 82 : for comment in comments {
364 82 : if !output.is_empty() && !ends_with_line_break(output) {
365 45 : output.push(b'\n');
366 47 : }
367 82 : let mut text = comment_text(input, comment);
368 82 : while let [rest @ .., last] = text {
369 82 : if !is_space(*last) {
370 82 : break;
371 0 : }
372 0 : text = rest;
373 : }
374 82 : output.extend_from_slice(text);
375 : }
376 70 : }
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 or ends the comments is kept, `following` is the offset of the text after
383 : /// the comments.
384 11612 : pub(super) fn append_comments(
385 11612 : output: &mut Vec<u8>,
386 11612 : input: &[u8],
387 11612 : comments: &[TextSpan],
388 11612 : max_width: usize,
389 11612 : following: Option<usize>,
390 11612 : ) {
391 11612 : let units = comment_units(input, comments);
392 11612 : let mut u = 0;
393 11812 : while u < units.len() {
394 200 : let unit = &units[u];
395 200 : if !unit.tagged {
396 166 : let slice = &comments[unit.comments.clone()];
397 166 : if is_block_comment(input, &slice[0]) {
398 64 : append_verbatim(output, input, slice);
399 104 : } else {
400 102 : append_wrapped_line_comments(output, input, slice, max_width);
401 102 : }
402 166 : if unit.comments.end == comments.len() {
403 137 : keep_blank_line(output, input, comments[comments.len() - 1].end() as usize, following);
404 137 : }
405 166 : u += 1;
406 166 : continue;
407 34 : }
408 : // The tagged units that follow after whitespace only
409 34 : let mut last = u;
410 37 : while last + 1 < units.len()
411 8 : && units[last + 1].tagged
412 3 : && is_whitespace(gap_between(input, &comments[units[last].comments.end - 1], &comments[units[last + 1].comments.start]))
413 3 : {
414 3 : last += 1;
415 3 : }
416 34 : let range = unit.comments.start..units[last].comments.end;
417 34 : if !output.is_empty() && !ends_with_line_break(output) {
418 1 : output.push(b'\n');
419 33 : }
420 34 : match print_tagged_comment(input, &comments[range.clone()], max_width) {
421 28 : Some(block) => output.extend_from_slice(&block),
422 : // Units that cannot be merged are printed one by one
423 : None => {
424 7 : for unit in &units[u..=last] {
425 7 : let slice = &comments[unit.comments.clone()];
426 7 : if !output.is_empty() && !ends_with_line_break(output) {
427 1 : output.push(b'\n');
428 6 : }
429 7 : match print_tagged_comment(input, slice, max_width) {
430 1 : Some(block) => output.extend_from_slice(&block),
431 6 : None => append_verbatim(output, input, slice),
432 : }
433 : }
434 : }
435 : }
436 34 : let next = comments.get(range.end).map(|c| c.offset as usize).or(following);
437 34 : keep_blank_line(output, input, comments[range.end - 1].end() as usize, next);
438 34 : u = last + 1;
439 : }
440 11612 : }
441 :
442 : /// Keep a blank line between the end of comments and the text that follows them at `next`
443 247 : fn keep_blank_line(output: &mut Vec<u8>, input: &[u8], text_end: usize, next: Option<usize>) {
444 247 : if let Some(next) = next.filter(|&next| next >= text_end) {
445 180 : if count_line_breaks(substr(input, text_end, next - text_end)) > 1 {
446 11 : output.extend_from_slice(b"\n\n");
447 169 : }
448 67 : }
449 247 : }
450 :
451 : /// Append the comments in front of a statement. With a tag among them they are its annotation: one canonical
452 : /// comment with the ordinary comments as the description on top, like a rewrite of a tag prints it (see
453 : /// `annotation`). Otherwise, or if the annotation cannot be printed canonically, they are appended like other
454 : /// comments. `following` is the offset of the statement.
455 169 : pub(super) fn append_description(output: &mut Vec<u8>, input: &[u8], comments: &[TextSpan], max_width: usize, following: Option<usize>) {
456 169 : let annotation = comment_units(input, comments)
457 169 : .iter()
458 169 : .any(|unit| unit.tagged)
459 169 : .then(|| print_tagged_comment(input, comments, max_width))
460 169 : .flatten();
461 169 : let Some(block) = annotation else {
462 93 : append_comments(output, input, comments, max_width, following);
463 93 : return;
464 : };
465 76 : if !output.is_empty() && !ends_with_line_break(output) {
466 0 : output.push(b'\n');
467 76 : }
468 76 : output.extend_from_slice(&block);
469 76 : keep_blank_line(output, input, comments[comments.len() - 1].end() as usize, following);
470 169 : }
471 :
472 : #[cfg(test)]
473 : mod tests {
474 : use super::*;
475 :
476 8 : fn spans(input: &str, comments: &[&str]) -> Vec<TextSpan> {
477 8 : let mut from = 0;
478 8 : comments
479 8 : .iter()
480 20 : .map(|c| {
481 20 : let offset = from + input[from..].find(c).unwrap();
482 20 : from = offset + c.len();
483 20 : TextSpan::new(offset as u32, c.len() as u32)
484 20 : })
485 8 : .collect()
486 8 : }
487 :
488 8 : fn format(input: &str, comments: &[&str], max_width: usize) -> String {
489 8 : let mut out = Vec::new();
490 8 : append_comments(&mut out, input.as_bytes(), &spans(input, comments), max_width, None);
491 8 : String::from_utf8(out).unwrap()
492 8 : }
493 :
494 : #[test]
495 1 : fn rewraps_line_comments() {
496 1 : let input = "-- aaa bbb\n-- ccc";
497 1 : assert_eq!(format(input, &["-- aaa bbb", "-- ccc"], 120), "-- aaa bbb ccc");
498 1 : assert_eq!(format(input, &["-- aaa bbb", "-- ccc"], 10), "-- aaa bbb\n-- ccc");
499 1 : assert_eq!(format(input, &["-- aaa bbb", "-- ccc"], 9), "-- aaa\n-- bbb\n-- ccc");
500 : // Words longer than the line are split
501 1 : assert_eq!(format("--abcdefgh", &["--abcdefgh"], 6), "-- abc\n-- def\n-- gh");
502 1 : }
503 :
504 : #[test]
505 1 : fn keeps_blank_lines_and_block_comments() {
506 1 : let input = "-- a\n\n-- b\n/* c */";
507 1 : assert_eq!(format(input, &["-- a", "-- b", "/* c */"], 120), "-- a\n-- b\n/* c */");
508 1 : let input = "-- a\n--\n-- b";
509 1 : assert_eq!(format(input, &["-- a", "--", "-- b"], 120), "-- a\n--\n-- b");
510 1 : }
511 :
512 : #[test]
513 1 : fn prints_tagged_regions() {
514 : // A tagged region ends with the run or with an end line, comments after it are rewrapped
515 1 : let input = "-- @vis markdown\n-- **{{a}}** x\n-- @end\n\n-- x y";
516 1 : let out = format(input, &["-- @vis markdown", "-- **{{a}}** x", "-- @end", "-- x y"], 120);
517 1 : assert_eq!(out, "/**\n * @vis markdown\n * **{{a}}** x\n */\n\n-- x y");
518 1 : let input = "-- @param Sales.\n-- @end\n-- x y";
519 1 : assert_eq!(format(input, &["-- @param Sales.", "-- @end", "-- x y"], 120), "/** @param Sales. */\n-- x y");
520 1 : }
521 :
522 : /// Format a script that consists of comments, every comment is found by the scanner
523 25 : fn format_comments(input: &str) -> String {
524 25 : format_comments_within(input, 120)
525 25 : }
526 :
527 26 : fn format_comments_within(input: &str, max_width: usize) -> String {
528 26 : let scanned = dashql_parser::scanner::scan(input, 0, 0);
529 26 : let mut out = Vec::new();
530 26 : append_comments(&mut out, input.as_bytes(), &scanned.comments, max_width, None);
531 26 : String::from_utf8(out).unwrap()
532 26 : }
533 :
534 : #[test]
535 1 : fn prints_vis_block_comments() {
536 1 : assert_eq!(format_comments("/* @board width=6 height=3 */"), "/** @board width=6 height=3 */");
537 1 : assert_eq!(format_comments("/* @flow x=1 y=2 */"), "/** @flow x=1 y=2 */");
538 : // The placements of older scripts become their own tags
539 1 : assert_eq!(format_comments("/* @vis board width=6 height=3 */"), "/** @board width=6 height=3 */");
540 1 : assert_eq!(format_comments("/* @vis card width=6 */"), "/** @board width=6 */");
541 1 : assert_eq!(format_comments("/* @vis table\n @vis flow x=1 y=2 */"), "/**\n * @vis table\n * @flow x=1 y=2\n */");
542 1 : assert_eq!(format_comments("/* @vis table */"), "/** @vis table */");
543 1 : let input = "/* @vis vegalite {\"mark\":\"bar\", \"encoding\": {\"x\": {\"field\": \"a\"}}} */";
544 1 : assert_eq!(format_comments(input), "/**\n * @vis vegalite\n * {\"mark\": \"bar\", \"encoding\": {\"x\": {\"field\": \"a\"}}}\n */");
545 : // The JSON breaks where it does not fit in the comment lines
546 1 : assert_eq!(
547 1 : format_comments_within(input, 40),
548 : "/**\n * @vis vegalite\n * {\n * \"mark\": \"bar\",\n * \"encoding\": {\"x\": {\"field\": \"a\"}}\n * }\n */"
549 : );
550 : // Formatting twice gives the same output
551 1 : let once = format_comments("/* @vis vegalite\n{\"mark\":\"bar\"}\n*/");
552 1 : assert_eq!(format_comments(&once), once);
553 1 : }
554 :
555 : #[test]
556 1 : fn prints_tagged_line_comments_as_block_comment() {
557 1 : let expected = "/**\n * @param Revenue.\n * @board width=6\n * @vis vegalite\n * {\"mark\": \"bar\"}\n */";
558 3 : for input in [
559 1 : "-- @param Revenue.\n-- @vis board width=6\n-- @vis vegalite\n-- {\"mark\": \"bar\"}",
560 1 : "-- @param Revenue.\n-- @vis board width=6\n-- @vis vegalite\n-- {\"mark\": \"bar\"}\n-- @end",
561 1 : "/* @param Revenue.\n @vis board width=6\n @vis vegalite\n {\"mark\": \"bar\"}\n*/",
562 1 : ] {
563 3 : assert_eq!(format_comments(input), expected, "{input}");
564 : }
565 1 : assert_eq!(format_comments(expected), expected);
566 : // Descriptions keep their lines, an empty line keeps its gutter
567 1 : let input = "-- @param\n-- Revenue.\n--\n-- * per region\n-- @end";
568 1 : assert_eq!(format_comments(input), "/**\n * @param\n * Revenue.\n *\n * * per region\n */");
569 1 : assert_eq!(format_comments("/* @param Revenue. */"), "/** @param Revenue. */");
570 1 : }
571 :
572 : #[test]
573 1 : fn merges_tagged_comments() {
574 1 : let input = "-- @param\n-- Add to timestamps.\n-- @end\n\n/* @vis vegalite\n {\"mark\": \"bar\"}\n*/";
575 1 : assert_eq!(format_comments(input), "/**\n * @param\n * Add to timestamps.\n * @vis vegalite\n * {\"mark\": \"bar\"}\n */");
576 : // Ordinary comments between them keep them apart
577 1 : let input = "/* @param Sales. */\n-- note\n/* @vis table */";
578 1 : assert_eq!(format_comments(input), "/** @param Sales. */\n-- note\n/** @vis table */");
579 : // The text before the first tag of a block comment goes in front of all tags
580 1 : let input = "/* @param Sales. */\n/* Note.\n @vis table */";
581 1 : assert_eq!(format_comments(input), "/**\n * Note.\n *\n * @param Sales.\n * @vis table\n */");
582 : // Comments that cannot be printed canonically stay apart
583 1 : let input = "/* @param Sales. */\n-- @vis markdown\n-- see */ here";
584 1 : assert_eq!(format_comments(input), "/** @param Sales. */\n-- @vis markdown\n-- see */ here");
585 1 : }
586 :
587 : #[test]
588 1 : fn keeps_text_before_the_first_tag() {
589 1 : let input = "/* Revenue dashboard.\n @vis table\n*/";
590 1 : assert_eq!(format_comments(input), "/**\n * Revenue dashboard.\n *\n * @vis table\n */");
591 : // Comments without tags stay as written
592 1 : let input = "/* Revenue dashboard.\n @see x\n*/";
593 1 : assert_eq!(format_comments(input), input);
594 1 : }
595 :
596 : #[test]
597 1 : fn keeps_markdown_and_malformed_json() {
598 1 : let input = "/**\n * @vis markdown\n * - {{a}}\n * - {{b}}\n */";
599 1 : assert_eq!(format_comments(input), input);
600 1 : let input = "/* @vis vegalite {\"mark\": */";
601 1 : assert_eq!(format_comments(input), "/** @vis vegalite {\"mark\": */");
602 1 : let input = "/* @vis vegalite\n {\"mark\":\n */";
603 1 : assert_eq!(format_comments(input), "/**\n * @vis vegalite\n * {\"mark\":\n */");
604 1 : }
605 :
606 : #[test]
607 1 : fn keeps_tagged_line_comments_that_cannot_be_block_comments() {
608 1 : let input = "-- @vis markdown\n-- see */ here";
609 1 : assert_eq!(format_comments(input), input);
610 1 : }
611 :
612 : #[test]
613 1 : fn trims_like_isspace() {
614 1 : assert_eq!(trim_whitespace(b"\x0b a \x0c"), b"a");
615 1 : assert!(is_whitespace(b" \t\r\n"));
616 1 : assert_eq!(substr(b"abc", 5, 2), b"");
617 1 : assert_eq!(substr(b"abc", 1, 10), b"bc");
618 1 : }
619 : }
|