LCOV - code coverage report
Current view: top level - dashql-core/src/formatter - comments.rs Coverage Total Hit
Test: DashQL 915828b Lines: 97.5 % 441 430
Test Date: 2026-11-06 19:30:32

            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              : }
        

Generated by: LCOV version 2.0-1