Skip to main content

genemichaels_lib/
whitespace.rs

1use {
2    crate::{
3        Comment,
4        CommentMode,
5        Whitespace,
6        WhitespaceMode,
7    },
8    loga::ea,
9    markdown::mdast::Node,
10    proc_macro2::{
11        Group,
12        LineColumn,
13        TokenStream,
14    },
15    regex::Regex,
16    std::{
17        cell::RefCell,
18        collections::BTreeMap,
19        hash::Hash,
20        rc::Rc,
21        str::FromStr,
22    },
23};
24
25#[derive(PartialEq, Eq, Debug, Clone, Copy)]
26pub struct HashLineColumn(pub LineColumn);
27
28impl Hash for HashLineColumn {
29    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
30        (self.0.line, self.0.column).hash(state);
31    }
32}
33
34impl Ord for HashLineColumn {
35    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
36        return self.0.line.cmp(&other.0.line).then(self.0.column.cmp(&other.0.column));
37    }
38}
39
40impl PartialOrd for HashLineColumn {
41    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
42        return Some(self.0.cmp(&other.0));
43    }
44}
45
46#[derive(Debug, derive_more::Add, PartialEq, Eq, PartialOrd, Ord, derive_more::Sub, Clone, Copy)]
47struct VisualLen(usize);
48
49fn unicode_len(text: &str) -> VisualLen {
50    VisualLen(text.chars().count())
51}
52
53/// Identifies the start/stop locations of whitespace in a chunk of source.
54/// Whitespace is grouped runs, but the `keep_max_blank_lines` parameter allows
55/// splitting the groups.
56pub fn extract_whitespaces(
57    keep_max_blank_lines: usize,
58    source: &str,
59) -> Result<(BTreeMap<HashLineColumn, Vec<Whitespace>>, TokenStream), loga::Error> {
60    let mut line_lookup = vec![];
61    {
62        let mut offset = 0usize;
63        loop {
64            line_lookup.push(offset);
65            offset += match source[offset..].find('\n') {
66                Some(r) => r,
67                None => {
68                    break;
69                },
70            } + 1;
71        }
72    }
73
74    struct State<'a> {
75        source: &'a str,
76        keep_max_blank_lines: usize,
77        // starting offset of each line
78        line_lookup: Vec<usize>,
79        whitespaces: BTreeMap<HashLineColumn, Vec<Whitespace>>,
80        // records the beginning of the last line extracted - this is the destination for
81        // transposed comments
82        line_start: Option<LineColumn>,
83        last_offset: usize,
84        start_re: Option<Regex>,
85        block_event_re: Option<Regex>,
86    }
87
88    impl<'a> State<'a> {
89        fn to_offset(&self, loc: LineColumn) -> usize {
90            if loc.line == 0 {
91                return 0usize;
92            }
93            let line_start_offset = *self.line_lookup.get(loc.line - 1).unwrap();
94            line_start_offset +
95                self.source[line_start_offset..].chars().take(loc.column).map(char::len_utf8).sum::<usize>()
96        }
97
98        fn add_comments(&mut self, end: LineColumn, abs_start: usize, between_ast_nodes: &str) {
99            let start_re = &self.start_re.get_or_insert_with(|| Regex::new(
100                // `//` maybe followed by `[/!.?]`, `/**/`, or `/*` maybe followed by `[*!]`
101                r#"(?:(//)(/|!|\.|\?)?)|(/\*\*/)|(?:(/\*)(\*|!)?)"#,
102            ).unwrap());
103            let block_event_re =
104                &self.block_event_re.get_or_insert_with(|| Regex::new(r#"((?:/\*)|(?:\*/))"#).unwrap());
105
106            struct CommentBuffer {
107                keep_max_blank_lines: usize,
108                blank_lines: usize,
109                out: Vec<Whitespace>,
110                mode: CommentMode,
111                lines: Vec<String>,
112                loc: LineColumn,
113                orig_start_offset: Option<usize>,
114            }
115
116            impl CommentBuffer {
117                fn flush(&mut self) {
118                    if self.lines.is_empty() {
119                        return;
120                    }
121                    self.out.push(Whitespace {
122                        loc: self.loc,
123                        mode: crate::WhitespaceMode::Comment(Comment {
124                            mode: self.mode,
125                            lines: self.lines.split_off(0).join("\n"),
126                            orig_start_offset: self.orig_start_offset.unwrap(),
127                        }),
128                    });
129                    self.blank_lines = 0;
130                    self.orig_start_offset = None;
131                }
132
133                fn add(&mut self, mode: CommentMode, line: &str, orig_start_offset: usize) {
134                    if self.mode != mode && !self.lines.is_empty() {
135                        self.flush();
136                    }
137                    self.mode = mode;
138                    self.lines.push(line.to_string());
139                    self.orig_start_offset.get_or_insert(orig_start_offset);
140                }
141
142                fn add_blank_lines(&mut self, text: &str) {
143                    let blank_lines = text.as_bytes().iter().filter(|x| **x == b'\n').count();
144                    if blank_lines > 1 && self.keep_max_blank_lines > 0 {
145                        self.flush();
146                        self.out.push(Whitespace {
147                            loc: self.loc,
148                            mode: crate::WhitespaceMode::BlankLines(
149                                (blank_lines - 1).min(self.keep_max_blank_lines),
150                            ),
151                        });
152                    }
153                }
154            }
155
156            let mut buffer = CommentBuffer {
157                keep_max_blank_lines: self.keep_max_blank_lines,
158                blank_lines: 0,
159                out: vec![],
160                mode: CommentMode::Normal,
161                lines: vec![],
162                loc: end,
163                orig_start_offset: None,
164            };
165            let mut text = (abs_start, between_ast_nodes);
166            'comment_loop : loop {
167                match start_re.captures(text.1) {
168                    Some(found_start) => {
169                        let orig_start_offset = abs_start + found_start.get(0).unwrap().start();
170                        let start_prefix_match =
171                            found_start
172                                .get(1)
173                                .or_else(|| found_start.get(3))
174                                .or_else(|| found_start.get(4))
175                                .unwrap();
176                        if buffer.out.is_empty() && buffer.lines.is_empty() {
177                            buffer.add_blank_lines(&text.1[..start_prefix_match.start()]);
178                        }
179                        match start_prefix_match.as_str() {
180                            "//" => {
181                                let mode = {
182                                    let start_suffix_match = found_start.get(2);
183                                    let (mut mode, mut match_end) = match start_suffix_match {
184                                        Some(start_suffix_match) => (match start_suffix_match.as_str() {
185                                            "/" => CommentMode::DocOuter,
186                                            "!" => CommentMode::DocInner,
187                                            "." => CommentMode::Verbatim,
188                                            "?" => CommentMode::ExplicitNormal,
189                                            _ => unreachable!(),
190                                        }, start_suffix_match.end()),
191                                        None => (CommentMode::Normal, start_prefix_match.end()),
192                                    };
193                                    if mode == CommentMode::DocOuter && text.1[match_end..].starts_with("/") {
194                                        // > 3 slashes, so actually not a doc comment
195                                        mode = CommentMode::Normal;
196                                        match_end = start_prefix_match.end();
197                                    }
198                                    text = (text.0 + match_end, &text.1[match_end..]);
199                                    mode
200                                };
201                                let (line, next_start) = match text.1.find('\n') {
202                                    Some(line_end) => (&text.1[..line_end], line_end + 1),
203                                    None => (text.1, text.1.len()),
204                                };
205                                buffer.add(mode, line, orig_start_offset);
206                                text = (text.0 + next_start, &text.1[next_start..]);
207                            },
208                            "/**/" => {
209                                buffer.add(CommentMode::Normal, "".into(), orig_start_offset);
210                                text = (text.0 + start_prefix_match.end(), &text.1[start_prefix_match.end()..]);
211                            },
212                            "/*" => {
213                                let mode = {
214                                    let start_suffix_match = found_start.get(5);
215                                    let (mode, match_end) = match start_suffix_match {
216                                        Some(start_suffix_match) => (match start_suffix_match.as_str() {
217                                            "*" => CommentMode::DocOuter,
218                                            "!" => CommentMode::DocInner,
219                                            _ => unreachable!(),
220                                        }, start_suffix_match.end()),
221                                        None => (CommentMode::Normal, start_prefix_match.end()),
222                                    };
223                                    text = (text.0 + match_end, &text.1[match_end..]);
224                                    mode
225                                };
226                                let mut nesting = 1;
227                                let mut search_end_at = 0usize;
228                                let (lines, next_start) = loop {
229                                    let found_event =
230                                        block_event_re.captures(&text.1[search_end_at..]).unwrap().get(1).unwrap();
231                                    let event_start = search_end_at + found_event.start();
232                                    search_end_at += found_event.end();
233                                    match found_event.as_str() {
234                                        "/*" => {
235                                            nesting += 1;
236                                        },
237                                        "*/" => {
238                                            nesting -= 1;
239                                            if nesting == 0 {
240                                                break (&text.1[..event_start], search_end_at);
241                                            }
242                                        },
243                                        _ => unreachable!(),
244                                    }
245                                };
246                                for line in lines.lines() {
247                                    let mut line = line.trim();
248                                    line = line.strip_prefix("* ").unwrap_or(line);
249                                    buffer.add(mode, line, orig_start_offset);
250                                }
251                                text = (text.0 + next_start, &text.1[next_start..]);
252                            },
253                            _ => unreachable!(),
254                        }
255                    },
256                    None => {
257                        if buffer.out.is_empty() && buffer.lines.is_empty() {
258                            buffer.add_blank_lines(text.1);
259                        }
260                        break 'comment_loop;
261                    },
262                }
263            }
264            buffer.flush();
265            if !buffer.out.is_empty() {
266                let whitespaces = self.whitespaces.entry(HashLineColumn(end)).or_insert(vec![]);
267
268                // Merge with existing comments (basically only if comments come before and after
269                // at the end of the line)
270                'merge : loop {
271                    let Some(previous_whitespace) = whitespaces.last_mut() else {
272                        break;
273                    };
274                    let WhitespaceMode::Comment(previous_comment) = &mut previous_whitespace.mode else {
275                        break;
276                    };
277                    let start = buffer.out.remove(0);
278                    loop {
279                        let WhitespaceMode::Comment(start_comment) = &start.mode else {
280                            break;
281                        };
282                        if previous_comment.mode != start_comment.mode {
283                            break;
284                        }
285                        previous_comment.lines.push_str("\n");
286                        previous_comment.lines.push_str(&start_comment.lines);
287                        break 'merge;
288                    }
289                    buffer.out.insert(0, start);
290                    break;
291                }
292
293                // Add rest without merging
294                whitespaces.extend(buffer.out);
295            }
296        }
297
298        fn extract(&mut self, mut start: usize, end: LineColumn) {
299            // Transpose line-end comments to line-start
300            if loop {
301                let previous_start = match &self.line_start {
302                    // No line start recorded, must be within first line
303                    None => break true,
304                    Some(s) => s,
305                };
306                if end.line <= previous_start.line {
307                    // Not at end of line yet, or at bad element (moved backwards); don't do anything
308                    break false;
309                }
310                let eol = match self.source[start..].find('\n') {
311                    Some(n) => start + n,
312                    None => self.source.len(),
313                };
314                let text = &self.source[start .. eol];
315                if text.trim_start().starts_with("//") {
316                    self.add_comments(*previous_start, start, text);
317                }
318                start = eol;
319                break true;
320            } {
321                self.line_start = Some(end);
322            }
323
324            // Do normal comment extraction
325            let end_offset = self.to_offset(end);
326            if end_offset < start {
327                return;
328            }
329            let whole_text = &self.source[start .. end_offset];
330            self.add_comments(end, start, whole_text);
331        }
332    }
333
334    // Extract comments
335    let mut state = State {
336        source: source,
337        keep_max_blank_lines: keep_max_blank_lines,
338        line_lookup: line_lookup,
339        whitespaces: BTreeMap::new(),
340        last_offset: 0usize,
341        line_start: None,
342        start_re: None,
343        block_event_re: None,
344    };
345
346    fn recurse(state: &mut State, ts: TokenStream) -> TokenStream {
347        let mut out = vec![];
348        let mut ts = ts.into_iter().peekable();
349        while let Some(t) = ts.next() {
350            match t {
351                proc_macro2::TokenTree::Group(g) => {
352                    state.extract(state.last_offset, g.span_open().start());
353                    state.last_offset = state.to_offset(g.span_open().end());
354                    let subtokens = recurse(state, g.stream());
355                    state.extract(state.last_offset, g.span_close().start());
356                    state.last_offset = state.to_offset(g.span_close().end());
357                    let mut new_g = Group::new(g.delimiter(), subtokens);
358                    new_g.set_span(g.span());
359                    out.push(proc_macro2::TokenTree::Group(new_g));
360                },
361                proc_macro2::TokenTree::Ident(g) => {
362                    state.extract(state.last_offset, g.span().start());
363                    state.last_offset = state.to_offset(g.span().end());
364                    out.push(proc_macro2::TokenTree::Ident(g));
365                },
366                proc_macro2::TokenTree::Punct(g) => {
367                    let offset = state.to_offset(g.span().start());
368                    if g.as_char() == '#' && &state.source[offset .. offset + 1] == "/" {
369                        // Syn converts doc comments into doc attrs, work around that here by detecting a
370                        // mismatch between the token and the source (written /, token is #) and skipping
371                        // all tokens within the fake doc attr range
372                        loop {
373                            let in_comment = ts.peek().map(|n| n.span().start() < g.span().end()).unwrap_or(false);
374                            if !in_comment {
375                                break;
376                            }
377                            ts.next();
378                        }
379                    } else {
380                        state.extract(state.last_offset, g.span().start());
381                        state.last_offset = state.to_offset(g.span().end());
382                        out.push(proc_macro2::TokenTree::Punct(g));
383                    }
384                },
385                proc_macro2::TokenTree::Literal(g) => {
386                    state.extract(state.last_offset, g.span().start());
387                    state.last_offset = state.to_offset(g.span().end());
388                    out.push(proc_macro2::TokenTree::Literal(g));
389                },
390            }
391        }
392        TokenStream::from_iter(out)
393    }
394
395    let tokens =
396        recurse(
397            &mut state,
398            TokenStream::from_str(
399                source,
400            ).map_err(
401                |e| loga::err_with(
402                    "Error undoing syn parse transformations",
403                    ea!(
404                        line = e.span().start().line,
405                        column = e.span().start().column,
406                        error = e.to_string(),
407                        source = source.lines().skip(e.span().start().line - 1).next().unwrap()
408                    ),
409                ),
410            )?,
411        );
412    state.add_comments(LineColumn {
413        line: 0,
414        column: 1,
415    }, state.last_offset, &source[state.last_offset..]);
416    Ok((state.whitespaces, tokens))
417}
418
419struct State {
420    line_buffer: String,
421    need_nl: bool,
422}
423
424#[derive(Debug)]
425struct LineState_ {
426    base_prefix_len: VisualLen,
427    first_prefix: Option<String>,
428    prefix: String,
429    max_width: VisualLen,
430    rel_max_width: Option<VisualLen>,
431    backward_break: Option<usize>,
432    unbreakable: bool,
433}
434
435impl LineState_ {
436    fn flush_always(&mut self, state: &mut State, out: &mut String) {
437        out.push_str(format!(
438            //. .
439            "{}{}{}",
440            if state.need_nl {
441                "\n"
442            } else {
443                ""
444            },
445            match &self.first_prefix.take() {
446                Some(t) => t,
447                None => &*self.prefix,
448            },
449            &state.line_buffer,
450        ).trim_end());
451        state.line_buffer.clear();
452        state.need_nl = true;
453        self.backward_break = None;
454    }
455
456    fn flush(&mut self, state: &mut State, out: &mut String) {
457        if !state.line_buffer.trim().is_empty() {
458            self.flush_always(state, out);
459        }
460    }
461
462    fn calc_max_width(&self) -> VisualLen {
463        match self.rel_max_width {
464            Some(w) => unicode_len(&self.prefix) + w,
465            None => self.max_width,
466        }
467    }
468
469    fn calc_current_len(&self, state: &State) -> VisualLen {
470        self.base_prefix_len + unicode_len(&state.line_buffer)
471    }
472}
473
474/// Line split points, must be interior indexes (no 0 and no text.len())
475fn get_splits(text: &str) -> Vec<usize> {
476    // let segmenter =
477    // LineBreakSegmenter::try_new_unstable(&icu_testdata::unstable()).unwrap(); match
478    // segmenter .segment_str(&text)
479    text.char_indices().filter(|i| i.1 == ' ').map(|i| i.0 + 1).collect()
480}
481
482struct LineState(Rc<RefCell<LineState_>>);
483
484impl LineState {
485    fn new(
486        base_prefix_len: VisualLen,
487        first_prefix: Option<String>,
488        prefix: String,
489        max_width: VisualLen,
490        rel_max_width: Option<VisualLen>,
491    ) -> LineState {
492        LineState(Rc::new(RefCell::new(LineState_ {
493            base_prefix_len: base_prefix_len,
494            first_prefix,
495            prefix,
496            max_width,
497            rel_max_width,
498            backward_break: None,
499            unbreakable: false,
500        })))
501    }
502
503    fn clone_inline(&self) -> LineState {
504        LineState(self.0.clone())
505    }
506
507    fn clone_zero_indent(&self) -> LineState {
508        let mut s = self.0.as_ref().borrow_mut();
509        LineState(Rc::new(RefCell::new(LineState_ {
510            base_prefix_len: s.base_prefix_len,
511            first_prefix: s.first_prefix.take(),
512            prefix: s.prefix.clone(),
513            max_width: s.max_width,
514            rel_max_width: s.rel_max_width,
515            backward_break: None,
516            unbreakable: false,
517        })))
518    }
519
520    fn clone_indent(&self, first_prefix: Option<String>, prefix: String) -> LineState {
521        let mut s = self.0.as_ref().borrow_mut();
522        LineState(Rc::new(RefCell::new(LineState_ {
523            base_prefix_len: s.base_prefix_len,
524            first_prefix: match (s.first_prefix.take(), first_prefix) {
525                (None, None) => None,
526                (None, Some(p)) => Some(format!("{}{}", s.prefix, p)),
527                (Some(p), None) => Some(p),
528                (Some(p1), Some(p2)) => Some(format!("{}{}", p1, p2)),
529            },
530            prefix: format!("{}{}", s.prefix, prefix),
531            max_width: s.max_width,
532            rel_max_width: s.rel_max_width,
533            backward_break: None,
534            unbreakable: false,
535        })))
536    }
537
538    fn clone_unbreakable(&self, first_prefix: Option<String>) -> LineState {
539        let mut s = self.0.as_ref().borrow_mut();
540        LineState(Rc::new(RefCell::new(LineState_ {
541            base_prefix_len: s.base_prefix_len,
542            first_prefix: match (s.first_prefix.take(), first_prefix) {
543                (None, None) => None,
544                (None, Some(p)) => Some(format!("{}{}", s.prefix, p)),
545                (Some(p), None) => Some(p),
546                (Some(p1), Some(p2)) => Some(format!("{}{}", p1, p2)),
547            },
548            prefix: s.prefix.clone(),
549            max_width: s.max_width,
550            rel_max_width: s.rel_max_width,
551            backward_break: None,
552            unbreakable: true,
553        })))
554    }
555
556    fn write(&self, state: &mut State, out: &mut String, text: &str, breaks: &[usize]) {
557        let mut s = self.0.as_ref().borrow_mut();
558        if s.unbreakable {
559            state.line_buffer.push_str(text);
560            return;
561        }
562        let max_len = s.calc_max_width();
563
564        struct FoundWritableLen<'a> {
565            /// How much of text can be written to the current line. If a break is used, will
566            /// be equal to the break point, but if the whole string is written may be longer
567            writable: usize,
568            /// If a break is used, the break and the remaining unused breaks
569            previous_break: Option<(usize, &'a [usize])>,
570            /// The next break after the last break before the max length, 2nd fallback (after
571            /// retro-break)
572            next_break: Option<(usize, &'a [usize])>,
573        }
574
575        fn find_writable_len<
576            'a,
577        >(
578            width: VisualLen,
579            max_len: VisualLen,
580            text: &str,
581            breaks_offset: usize,
582            breaks: &'a [usize],
583        ) -> FoundWritableLen<'a> {
584            let mut previous_break = None;
585            let mut writable = 0;
586            for (i, b) in breaks.iter().enumerate() {
587                let b = *b - breaks_offset;
588                let next_break = Some((b, &breaks[i + 1..]));
589                if width + unicode_len(&text[..b]) > max_len {
590                    return FoundWritableLen {
591                        writable: writable,
592                        previous_break: previous_break,
593                        next_break: next_break,
594                    };
595                }
596                previous_break = next_break;
597                writable = b;
598            }
599            return FoundWritableLen {
600                writable: if width + unicode_len(&text) > max_len {
601                    writable
602                } else {
603                    text.len()
604                },
605                previous_break: previous_break,
606                next_break: None,
607            };
608        }
609
610        /// Write new text following a break, storing the new break point with it
611        fn write_forward(state: &mut State, s: &mut LineState_, text: &str, b: Option<usize>) {
612            if let Some(b) = b {
613                s.backward_break = Some(state.line_buffer.len() + b);
614            }
615            state.line_buffer.push_str(&text);
616        }
617
618        fn write_forward_breaks(
619            state: &mut State,
620            s: &mut LineState_,
621            out: &mut String,
622            max_len: VisualLen,
623            mut first: bool,
624            mut text: String,
625            mut breaks_offset: usize,
626            breaks: &[usize],
627        ) {
628            let mut breaks = breaks;
629            while !text.is_empty() {
630                if first {
631                    first = false;
632                } else {
633                    s.flush(state, out);
634                }
635                let found = find_writable_len(s.calc_current_len(state), max_len, &text, breaks_offset, breaks);
636                if found.writable > 0 {
637                    write_forward(state, s, &text[..found.writable], found.previous_break.map(|b| b.0));
638                    breaks = found.previous_break.map(|b| b.1).unwrap_or(breaks);
639                    text = text.split_off(found.writable);
640                    breaks_offset += found.writable;
641                } else if let Some((b, breaks0)) = found.next_break {
642                    write_forward(state, s, &text[..b], Some(b));
643                    breaks = breaks0;
644                    text = text.split_off(b);
645                    breaks_offset += b;
646                } else {
647                    state.line_buffer.push_str(&text);
648                    return;
649                }
650            }
651        }
652
653        let found = find_writable_len(s.calc_current_len(state), max_len, text, 0, breaks);
654        if found.writable > 0 {
655            write_forward(state, &mut s, &text[..found.writable], found.previous_break.map(|b| b.0));
656            write_forward_breaks(
657                state,
658                &mut s,
659                out,
660                max_len,
661                false,
662                (&text[found.writable..]).to_string(),
663                found.writable,
664                found.previous_break.map(|b| b.1).unwrap_or(breaks),
665            );
666        } else if let Some(at) = s.backward_break.take() {
667            // Couldn't split forward but there's a retroactive split point in previously
668            // written segments
669            let prefix = state.line_buffer.split_off(at);
670            s.flush(state, out);
671            state.line_buffer.push_str(&prefix);
672            write_forward_breaks(state, &mut s, out, max_len, true, text.to_string(), 0, breaks);
673        } else if let Some((b, breaks)) = found.next_break {
674            // No retroactive split, try first split after max len (overflow, but better than
675            // nothing)
676            write_forward(state, &mut s, &text[..b], Some(b));
677            write_forward_breaks(state, &mut s, out, max_len, false, (&text[b..]).to_string(), b, breaks);
678        } else {
679            state.line_buffer.push_str(text);
680        }
681    }
682
683    fn write_breakable(&self, state: &mut State, out: &mut String, text: &str) {
684        self.write(state, out, text, &get_splits(text));
685    }
686
687    fn write_unbreakable(&self, state: &mut State, out: &mut String, text: &str) {
688        self.write(state, out, text, &[]);
689    }
690
691    fn flush_always(&self, state: &mut State, out: &mut String) {
692        self.0.as_ref().borrow_mut().flush_always(state, out);
693    }
694
695    fn write_newline(&self, state: &mut State, out: &mut String) {
696        let mut s = self.0.as_ref().borrow_mut();
697        if !state.line_buffer.is_empty() {
698            panic!();
699        }
700        s.flush_always(state, out);
701    }
702}
703
704fn recurse_write(state: &mut State, out: &mut String, line: LineState, node: &Node, inline: bool) {
705    fn join_lines(text: &str) -> String {
706        let lines = Regex::new("\r?\n").unwrap().split(text).collect::<Vec<&str>>();
707        let mut joined = String::new();
708        for (i, line) in lines.iter().enumerate() {
709            let mut line = *line;
710            if i > 0 {
711                line = line.trim_start();
712                joined.push(' ');
713            }
714            if i < lines.len() - 1 {
715                line = line.trim_end();
716            }
717            joined.push_str(line);
718        }
719        joined
720    }
721
722    match node {
723        // block->block elements (newline between)
724        Node::Root(x) => {
725            for (i, child) in x.children.iter().enumerate() {
726                if i > 0 {
727                    line.write_newline(state, out);
728                }
729                recurse_write(state, out, line.clone_zero_indent(), child, false);
730            }
731        },
732        Node::Blockquote(x) => {
733            let line = line.clone_indent(None, "> ".into());
734            for (i, child) in x.children.iter().enumerate() {
735                if i > 0 {
736                    line.write_newline(state, out);
737                }
738                recurse_write(state, out, line.clone_inline(), child, false);
739            }
740        },
741        Node::List(x) => {
742            match &x.start {
743                Some(i) => {
744                    // bug in markdown lib, start is actually the number of the last child:
745                    // https://github.com/wooorm/markdown-rs/issues/38
746                    for (j, child) in x.children.iter().enumerate() {
747                        if j > 0 {
748                            line.write_newline(state, out);
749                        }
750                        recurse_write(
751                            state,
752                            out,
753                            line.clone_indent(Some(format!("{}. ", *i as usize + j)), "   ".into()),
754                            child,
755                            false,
756                        );
757                    }
758                },
759                None => {
760                    for (i, child) in x.children.iter().enumerate() {
761                        if i > 0 {
762                            line.write_newline(state, out);
763                        }
764                        recurse_write(state, out, line.clone_indent(Some("* ".into()), "  ".into()), child, false);
765                    }
766                },
767            };
768        },
769        Node::ListItem(x) => {
770            for (i, child) in x.children.iter().enumerate() {
771                if i > 0 {
772                    line.write_newline(state, out);
773                }
774                recurse_write(state, out, line.clone_zero_indent(), child, false);
775            }
776        },
777        // block->inline elements (flush after)
778        Node::Code(x) => {
779            line.write_unbreakable(state, out, &format!("```{}", match &x.lang {
780                None => "",
781                Some(x) => x,
782            }));
783            line.flush_always(state, out);
784            for l in x.value.as_str().lines() {
785                line.write_unbreakable(state, out, l);
786                line.flush_always(state, out);
787            }
788            line.write_unbreakable(state, out, "```");
789            line.flush_always(state, out);
790        },
791        Node::Heading(x) => {
792            let line = line.clone_unbreakable(Some(format!("{} ", "#".repeat(x.depth as usize))));
793            for child in &x.children {
794                recurse_write(state, out, line.clone_inline(), child, true);
795            }
796            line.flush_always(state, out);
797        },
798        Node::FootnoteDefinition(x) => {
799            let line = line.clone_indent(Some(format!("[^{}]: ", x.identifier)), "   ".into());
800            for child in &x.children {
801                recurse_write(state, out, line.clone_inline(), child, true);
802            }
803            line.flush_always(state, out);
804        },
805        Node::ThematicBreak(_) => {
806            line.write_unbreakable(state, out, "---");
807            line.flush_always(state, out);
808        },
809        Node::Definition(x) => {
810            line.write_unbreakable(state, out, &format!("[{}]: {}", x.identifier.trim(), x.url));
811            if let Some(title) = &x.title {
812                line.write_unbreakable(state, out, " \"");
813                line.write_breakable(state, out, title);
814                line.write_unbreakable(state, out, "\"");
815            }
816            line.flush_always(state, out);
817        },
818        Node::Paragraph(x) => {
819            for child in &x.children {
820                recurse_write(state, out, line.clone_inline(), child, true);
821            }
822            line.flush_always(state, out);
823        },
824        Node::Html(x) if !inline => {
825            line.write_unbreakable(state, out, &format!("`{}`", join_lines(&x.value)));
826            line.flush_always(state, out);
827        },
828        // inline elements
829        Node::Text(x) => {
830            line.write_breakable(state, out, &join_lines(&x.value));
831        },
832        Node::InlineCode(x) => {
833            line.write_unbreakable(state, out, &format!("`{}`", join_lines(&x.value)));
834        },
835        Node::Strong(x) => {
836            line.write_unbreakable(state, out, "**");
837            for child in &x.children {
838                recurse_write(state, out, line.clone_inline(), child, true);
839            }
840            line.write_unbreakable(state, out, "**");
841        },
842        Node::Delete(x) => {
843            line.write_unbreakable(state, out, "~~");
844            for child in &x.children {
845                recurse_write(state, out, line.clone_inline(), child, true);
846            }
847            line.write_unbreakable(state, out, "~~");
848        },
849        Node::Emphasis(x) => {
850            line.write_unbreakable(state, out, "_");
851            for child in &x.children {
852                recurse_write(state, out, line.clone_inline(), child, true);
853            }
854            line.write_unbreakable(state, out, "_");
855        },
856        Node::FootnoteReference(x) => {
857            line.write_unbreakable(state, out, &format!("[^{}]", x.identifier));
858        },
859        Node::Html(x) => {
860            line.write_unbreakable(state, out, &format!("`{}`", join_lines(&x.value)));
861        },
862        Node::Image(x) => {
863            let alt = join_lines(&x.alt);
864            match (get_splits(&join_lines(&alt)).first().is_some(), &x.title) {
865                (false, None) => {
866                    line.write_unbreakable(state, out, &format!("![{}]({})", alt, x.url));
867                },
868                (false, Some(t)) => {
869                    line.write_unbreakable(state, out, &format!("![{}]({}", alt, x.url));
870                    line.write_unbreakable(state, out, " \"");
871                    line.write_breakable(state, out, &join_lines(t));
872                    line.write_unbreakable(state, out, "\")");
873                },
874                (true, None) => {
875                    line.write_unbreakable(state, out, "![");
876                    line.write_breakable(state, out, &alt);
877                    line.write_unbreakable(state, out, &format!("]({})", x.url));
878                },
879                (true, Some(t)) => {
880                    line.write_unbreakable(state, out, "![");
881                    line.write_breakable(state, out, &alt);
882                    line.write_unbreakable(state, out, &format!("]({}", x.url));
883                    line.write_unbreakable(state, out, " \"");
884                    line.write_breakable(state, out, &join_lines(t));
885                    line.write_unbreakable(state, out, "\")");
886                },
887            }
888        },
889        Node::ImageReference(x) => {
890            line.write_unbreakable(state, out, &format!("![][{}]", x.identifier));
891        },
892        Node::Link(x) => {
893            let simple_text = if x.children.len() != 1 {
894                None
895            } else {
896                x.children.get(0)
897            }.and_then(|c| match c {
898                Node::Text(t) => {
899                    let t = join_lines(&t.value);
900                    if get_splits(&t).first().is_some() {
901                        None
902                    } else {
903                        Some(t)
904                    }
905                },
906                Node::InlineCode(t) => {
907                    let t = join_lines(&t.value);
908                    if get_splits(&t).first().is_some() {
909                        None
910                    } else {
911                        Some(format!("`{}`", t))
912                    }
913                },
914                _ => None,
915            });
916            match (simple_text, &x.title) {
917                (Some(unbroken_content), None) => {
918                    if unbroken_content.as_str() == x.url.as_str() {
919                        line.write_unbreakable(state, out, &format!("<{}>", x.url));
920                    } else {
921                        line.write_unbreakable(state, out, &format!("[{}]({})", unbroken_content, x.url));
922                    }
923                },
924                (Some(c), Some(title)) => {
925                    line.write_unbreakable(state, out, &format!("[{}]({}", c, x.url));
926                    line.write_unbreakable(state, out, " \"");
927                    line.write_breakable(state, out, title);
928                    line.write_unbreakable(state, out, "\")");
929                },
930                (None, None) => {
931                    line.write_unbreakable(state, out, "[");
932                    for child in &x.children {
933                        recurse_write(state, out, line.clone_inline(), child, true);
934                    }
935                    line.write_unbreakable(state, out, &format!("]({})", x.url));
936                },
937                (None, Some(title)) => {
938                    line.write_unbreakable(state, out, "[");
939                    for child in &x.children {
940                        recurse_write(state, out, line.clone_inline(), child, true);
941                    }
942                    line.write_unbreakable(state, out, &format!("]({}", x.url));
943                    line.write_unbreakable(state, out, " \"");
944                    line.write_breakable(state, out, title);
945                    line.write_unbreakable(state, out, "\")");
946                },
947            }
948        },
949        Node::LinkReference(x) => {
950            let simple_text = if x.children.len() != 1 {
951                None
952            } else {
953                x.children.get(0)
954            }.and_then(|c| match c {
955                Node::Text(t) => if get_splits(&t.value).first().is_some() {
956                    None
957                } else {
958                    Some(t.value.clone())
959                },
960                Node::InlineCode(t) => if get_splits(&t.value).first().is_some() {
961                    None
962                } else {
963                    Some(format!("`{}`", t.value))
964                },
965                _ => {
966                    None
967                },
968            });
969            match simple_text {
970                Some(t) if t == x.identifier => {
971                    line.write_unbreakable(state, out, &format!("[{}]", t));
972                },
973                _ => {
974                    line.write_unbreakable(state, out, "[");
975                    for child in &x.children {
976                        recurse_write(state, out, line.clone_inline(), child, true);
977                    }
978                    line.write_unbreakable(state, out, &format!("][{}]", x.identifier));
979                },
980            }
981        },
982        Node::Break(_) => {
983            // normalized out
984        },
985        Node::Math(_) => unreachable!(),
986        Node::Table(_) => unreachable!(),
987        Node::TableRow(_) => unreachable!(),
988        Node::TableCell(_) => unreachable!(),
989        Node::MdxJsxTextElement(_) => unreachable!(),
990        Node::MdxFlowExpression(_) => unreachable!(),
991        Node::MdxJsxFlowElement(_) => unreachable!(),
992        Node::MdxjsEsm(_) => unreachable!(),
993        Node::Toml(_) => unreachable!(),
994        Node::Yaml(_) => unreachable!(),
995        Node::InlineMath(_) => unreachable!(),
996        Node::MdxTextExpression(_) => unreachable!(),
997    }
998}
999
1000pub fn format_md(
1001    true_out: &mut String,
1002    max_width: usize,
1003    rel_max_width: Option<usize>,
1004    prefix: &str,
1005    source: &str,
1006) -> Result<(), loga::Error> {
1007    // TODO, due to a bug a bunch of unreachable branches might have had code added.
1008    // I'd like to go back and see if some block-level starts can be removed in
1009    // contexts they shouldn't appear.
1010    match || -> Result<String, loga::Error> {
1011        let mut out = String::new();
1012        let mut state = State {
1013            line_buffer: String::new(),
1014            need_nl: false,
1015        };
1016        let ast = markdown::to_mdast(source, &markdown::ParseOptions {
1017            constructs: markdown::Constructs { ..Default::default() },
1018            ..Default::default()
1019        }).map_err(|e| loga::err_with("Error parsing markdown", ea!(err = e)))?;
1020        recurse_write(
1021            &mut state,
1022            &mut out,
1023            LineState::new(
1024                unicode_len(&prefix),
1025                None,
1026                prefix.to_string(),
1027                VisualLen(max_width),
1028                rel_max_width.map(VisualLen),
1029            ),
1030            &ast,
1031            false,
1032        );
1033        Ok(out)
1034    }() {
1035        Ok(o) => {
1036            true_out.push_str(&o);
1037            Ok(())
1038        },
1039        Err(e) => {
1040            Err(e)
1041        },
1042    }
1043}