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    explicit_wrap: bool,
430    max_width: VisualLen,
431    rel_max_width: Option<VisualLen>,
432    backward_break: Option<(usize, bool)>,
433}
434
435impl LineState_ {
436    fn flush_always(&mut self, state: &mut State, out: &mut String, explicit_wrap: bool, wrapping: bool) {
437        out.push_str(format!("{}{}{}{}", if state.need_nl {
438            "\n"
439        } else {
440            ""
441        }, match &self.first_prefix.take() {
442            Some(t) => t,
443            None => &*self.prefix,
444        }, &state.line_buffer, if wrapping && explicit_wrap {
445            " \\"
446        } else {
447            ""
448        }).trim_end());
449        state.line_buffer.clear();
450        state.need_nl = true;
451        self.backward_break = None;
452    }
453
454    fn flush(&mut self, state: &mut State, out: &mut String, explicit_wrap: bool, wrapping: bool) {
455        if !state.line_buffer.trim().is_empty() {
456            self.flush_always(state, out, explicit_wrap, wrapping);
457        }
458    }
459
460    fn calc_max_width(&self) -> VisualLen {
461        match self.rel_max_width {
462            Some(w) => unicode_len(&self.prefix) + w,
463            None => self.max_width - if self.explicit_wrap {
464                VisualLen(2)
465            } else {
466                VisualLen(0)
467            },
468        }
469    }
470
471    fn calc_current_len(&self, state: &State) -> VisualLen {
472        self.base_prefix_len + unicode_len(&state.line_buffer)
473    }
474}
475
476/// Line split points, must be interior indexes (no 0 and no text.len())
477fn get_splits(text: &str) -> Vec<usize> {
478    // let segmenter =
479    // LineBreakSegmenter::try_new_unstable(&icu_testdata::unstable()).unwrap(); match
480    // segmenter .segment_str(&text)
481    text.char_indices().filter(|i| i.1 == ' ').map(|i| i.0 + 1).collect()
482}
483
484struct LineState(Rc<RefCell<LineState_>>);
485
486impl LineState {
487    fn new(
488        base_prefix_len: VisualLen,
489        first_prefix: Option<String>,
490        prefix: String,
491        max_width: VisualLen,
492        rel_max_width: Option<VisualLen>,
493        explicit_wrap: bool,
494    ) -> LineState {
495        LineState(Rc::new(RefCell::new(LineState_ {
496            base_prefix_len: base_prefix_len,
497            first_prefix,
498            prefix,
499            explicit_wrap,
500            max_width,
501            rel_max_width,
502            backward_break: None,
503        })))
504    }
505
506    fn clone_inline(&self) -> LineState {
507        LineState(self.0.clone())
508    }
509
510    fn clone_zero_indent(&self) -> LineState {
511        let mut s = self.0.as_ref().borrow_mut();
512        LineState(Rc::new(RefCell::new(LineState_ {
513            base_prefix_len: s.base_prefix_len,
514            first_prefix: s.first_prefix.take(),
515            prefix: s.prefix.clone(),
516            explicit_wrap: s.explicit_wrap,
517            max_width: s.max_width,
518            rel_max_width: s.rel_max_width,
519            backward_break: None,
520        })))
521    }
522
523    fn clone_indent(&self, first_prefix: Option<String>, prefix: String, explicit_wrap: bool) -> LineState {
524        let mut s = self.0.as_ref().borrow_mut();
525        LineState(Rc::new(RefCell::new(LineState_ {
526            base_prefix_len: s.base_prefix_len,
527            first_prefix: match (s.first_prefix.take(), first_prefix) {
528                (None, None) => None,
529                (None, Some(p)) => Some(format!("{}{}", s.prefix, p)),
530                (Some(p), None) => Some(p),
531                (Some(p1), Some(p2)) => Some(format!("{}{}", p1, p2)),
532            },
533            prefix: format!("{}{}", s.prefix, prefix),
534            explicit_wrap: s.explicit_wrap || explicit_wrap,
535            max_width: s.max_width,
536            rel_max_width: s.rel_max_width,
537            backward_break: None,
538        })))
539    }
540
541    fn write(&self, state: &mut State, out: &mut String, text: &str, breaks: &[usize]) {
542        let mut s = self.0.as_ref().borrow_mut();
543        let max_len = s.calc_max_width();
544
545        struct FoundWritableLen<'a> {
546            /// How much of text can be written to the current line. If a break is used, will
547            /// be equal to the break point, but if the whole string is written may be longer
548            writable: usize,
549            /// If a break is used, the break and the remaining unused breaks
550            previous_break: Option<(usize, &'a [usize])>,
551            /// The next break after the last break before the max length, 2nd fallback (after
552            /// retro-break)
553            next_break: Option<(usize, &'a [usize])>,
554        }
555
556        fn find_writable_len<
557            'a,
558        >(
559            width: VisualLen,
560            max_len: VisualLen,
561            text: &str,
562            breaks_offset: usize,
563            breaks: &'a [usize],
564        ) -> FoundWritableLen<'a> {
565            let mut previous_break = None;
566            let mut writable = 0;
567            for (i, b) in breaks.iter().enumerate() {
568                let b = *b - breaks_offset;
569                let next_break = Some((b, &breaks[i + 1..]));
570                if width + unicode_len(&text[..b]) > max_len {
571                    return FoundWritableLen {
572                        writable: writable,
573                        previous_break: previous_break,
574                        next_break: next_break,
575                    };
576                }
577                previous_break = next_break;
578                writable = b;
579            }
580            return FoundWritableLen {
581                writable: if width + unicode_len(&text) > max_len {
582                    writable
583                } else {
584                    text.len()
585                },
586                previous_break: previous_break,
587                next_break: None,
588            };
589        }
590
591        /// Write new text following a break, storing the new break point with it
592        fn write_forward(state: &mut State, s: &mut LineState_, text: &str, b: Option<usize>) {
593            if let Some(b) = b {
594                s.backward_break = Some((state.line_buffer.len() + b, s.explicit_wrap));
595            }
596            state.line_buffer.push_str(&text);
597        }
598
599        fn write_forward_breaks(
600            state: &mut State,
601            s: &mut LineState_,
602            out: &mut String,
603            max_len: VisualLen,
604            mut first: bool,
605            mut text: String,
606            mut breaks_offset: usize,
607            breaks: &[usize],
608        ) {
609            let mut breaks = breaks;
610            while !text.is_empty() {
611                if first {
612                    first = false;
613                } else {
614                    s.flush(state, out, s.explicit_wrap, true);
615                }
616                let found = find_writable_len(s.calc_current_len(state), max_len, &text, breaks_offset, breaks);
617                if found.writable > 0 {
618                    write_forward(state, s, &text[..found.writable], found.previous_break.map(|b| b.0));
619                    breaks = found.previous_break.map(|b| b.1).unwrap_or(breaks);
620                    text = text.split_off(found.writable);
621                    breaks_offset += found.writable;
622                } else if let Some((b, breaks0)) = found.next_break {
623                    write_forward(state, s, &text[..b], Some(b));
624                    breaks = breaks0;
625                    text = text.split_off(b);
626                    breaks_offset += b;
627                } else {
628                    state.line_buffer.push_str(&text);
629                    return;
630                }
631            }
632        }
633
634        let found = find_writable_len(s.calc_current_len(state), max_len, text, 0, breaks);
635        if found.writable > 0 {
636            write_forward(state, &mut s, &text[..found.writable], found.previous_break.map(|b| b.0));
637            write_forward_breaks(
638                state,
639                &mut s,
640                out,
641                max_len,
642                false,
643                (&text[found.writable..]).to_string(),
644                found.writable,
645                found.previous_break.map(|b| b.1).unwrap_or(breaks),
646            );
647        } else if let Some((at, explicit_wrap)) = s.backward_break.take() {
648            // Couldn't split forward but there's a retroactive split point in previously
649            // written segments
650            let prefix = state.line_buffer.split_off(at);
651            s.flush(state, out, explicit_wrap, true);
652            state.line_buffer.push_str(&prefix);
653            write_forward_breaks(state, &mut s, out, max_len, true, text.to_string(), 0, breaks);
654        } else if let Some((b, breaks)) = found.next_break {
655            // No retroactive split, try first split after max len (overflow, but better than
656            // nothing)
657            write_forward(state, &mut s, &text[..b], Some(b));
658            write_forward_breaks(state, &mut s, out, max_len, false, (&text[b..]).to_string(), b, breaks);
659        } else {
660            state.line_buffer.push_str(text);
661            return;
662        }
663    }
664
665    fn write_breakable(&self, state: &mut State, out: &mut String, text: &str) {
666        self.write(state, out, text, &get_splits(text));
667    }
668
669    fn write_unbreakable(&self, state: &mut State, out: &mut String, text: &str) {
670        self.write(state, out, text, &[]);
671    }
672
673    fn flush_always(&self, state: &mut State, out: &mut String) {
674        self.0.as_ref().borrow_mut().flush_always(state, out, false, false);
675    }
676
677    fn write_newline(&self, state: &mut State, out: &mut String) {
678        let mut s = self.0.as_ref().borrow_mut();
679        if !state.line_buffer.is_empty() {
680            panic!();
681        }
682        s.flush_always(state, out, false, false);
683    }
684}
685
686fn recurse_write(state: &mut State, out: &mut String, line: LineState, node: &Node, inline: bool) {
687    fn join_lines(text: &str) -> String {
688        let lines = Regex::new("\r?\n").unwrap().split(text).collect::<Vec<&str>>();
689        let mut joined = String::new();
690        for (i, line) in lines.iter().enumerate() {
691            let mut line = *line;
692            if i > 0 {
693                line = line.trim_start();
694                joined.push(' ');
695            }
696            if i < lines.len() - 1 {
697                line = line.trim_end();
698            }
699            joined.push_str(line);
700        }
701        joined
702    }
703
704    match node {
705        // block->block elements (newline between)
706        Node::Root(x) => {
707            for (i, child) in x.children.iter().enumerate() {
708                if i > 0 {
709                    line.write_newline(state, out);
710                }
711                recurse_write(state, out, line.clone_zero_indent(), child, false);
712            }
713        },
714        Node::Blockquote(x) => {
715            let line = line.clone_indent(None, "> ".into(), false);
716            for (i, child) in x.children.iter().enumerate() {
717                if i > 0 {
718                    line.write_newline(state, out);
719                }
720                recurse_write(state, out, line.clone_inline(), child, false);
721            }
722        },
723        Node::List(x) => {
724            match &x.start {
725                Some(i) => {
726                    // bug in markdown lib, start is actually the number of the last child:
727                    // https://github.com/wooorm/markdown-rs/issues/38
728                    for (j, child) in x.children.iter().enumerate() {
729                        if j > 0 {
730                            line.write_newline(state, out);
731                        }
732                        recurse_write(
733                            state,
734                            out,
735                            line.clone_indent(Some(format!("{}. ", *i as usize + j)), "   ".into(), false),
736                            child,
737                            false,
738                        );
739                    }
740                },
741                None => {
742                    for (i, child) in x.children.iter().enumerate() {
743                        if i > 0 {
744                            line.write_newline(state, out);
745                        }
746                        recurse_write(
747                            state,
748                            out,
749                            line.clone_indent(Some("* ".into()), "  ".into(), false),
750                            child,
751                            false,
752                        );
753                    }
754                },
755            };
756        },
757        Node::ListItem(x) => {
758            for (i, child) in x.children.iter().enumerate() {
759                if i > 0 {
760                    line.write_newline(state, out);
761                }
762                recurse_write(state, out, line.clone_zero_indent(), child, false);
763            }
764        },
765        // block->inline elements (flush after)
766        Node::Code(x) => {
767            line.write_unbreakable(state, out, &format!("```{}", match &x.lang {
768                None => "",
769                Some(x) => x,
770            }));
771            line.flush_always(state, out);
772            for l in x.value.as_str().lines() {
773                line.write_unbreakable(state, out, l);
774                line.flush_always(state, out);
775            }
776            line.write_unbreakable(state, out, "```");
777            line.flush_always(state, out);
778        },
779        Node::Heading(x) => {
780            let line = line.clone_indent(Some(format!("{} ", "#".repeat(x.depth as usize))), "  ".into(), true);
781            for child in &x.children {
782                recurse_write(state, out, line.clone_inline(), child, true);
783            }
784            line.flush_always(state, out);
785        },
786        Node::FootnoteDefinition(x) => {
787            let line = line.clone_indent(Some(format!("[^{}]: ", x.identifier)), "   ".into(), false);
788            for child in &x.children {
789                recurse_write(state, out, line.clone_inline(), child, true);
790            }
791            line.flush_always(state, out);
792        },
793        Node::ThematicBreak(_) => {
794            line.write_unbreakable(state, out, "---");
795            line.flush_always(state, out);
796        },
797        Node::Definition(x) => {
798            line.write_unbreakable(state, out, &format!("[{}]: {}", x.identifier.trim(), x.url));
799            if let Some(title) = &x.title {
800                line.write_unbreakable(state, out, " \"");
801                line.write_breakable(state, out, title);
802                line.write_unbreakable(state, out, "\"");
803            }
804            line.flush_always(state, out);
805        },
806        Node::Paragraph(x) => {
807            for child in &x.children {
808                recurse_write(state, out, line.clone_inline(), child, true);
809            }
810            line.flush_always(state, out);
811        },
812        Node::Html(x) if !inline => {
813            line.write_unbreakable(state, out, &format!("`{}`", join_lines(&x.value)));
814            line.flush_always(state, out);
815        },
816        // inline elements
817        Node::Text(x) => {
818            line.write_breakable(state, out, &join_lines(&x.value));
819        },
820        Node::InlineCode(x) => {
821            line.write_unbreakable(state, out, &format!("`{}`", join_lines(&x.value)));
822        },
823        Node::Strong(x) => {
824            line.write_unbreakable(state, out, "**");
825            for child in &x.children {
826                recurse_write(state, out, line.clone_inline(), child, true);
827            }
828            line.write_unbreakable(state, out, "**");
829        },
830        Node::Delete(x) => {
831            line.write_unbreakable(state, out, "~~");
832            for child in &x.children {
833                recurse_write(state, out, line.clone_inline(), child, true);
834            }
835            line.write_unbreakable(state, out, "~~");
836        },
837        Node::Emphasis(x) => {
838            line.write_unbreakable(state, out, "_");
839            for child in &x.children {
840                recurse_write(state, out, line.clone_inline(), child, true);
841            }
842            line.write_unbreakable(state, out, "_");
843        },
844        Node::FootnoteReference(x) => {
845            line.write_unbreakable(state, out, &format!("[^{}]", x.identifier));
846        },
847        Node::Html(x) => {
848            line.write_unbreakable(state, out, &format!("`{}`", join_lines(&x.value)));
849        },
850        Node::Image(x) => {
851            let alt = join_lines(&x.alt);
852            match (get_splits(&join_lines(&alt)).first().is_some(), &x.title) {
853                (false, None) => {
854                    line.write_unbreakable(state, out, &format!("![{}]({})", alt, x.url));
855                },
856                (false, Some(t)) => {
857                    line.write_unbreakable(state, out, &format!("![{}]({}", alt, x.url));
858                    line.write_unbreakable(state, out, " \"");
859                    line.write_breakable(state, out, &join_lines(t));
860                    line.write_unbreakable(state, out, "\")");
861                },
862                (true, None) => {
863                    line.write_unbreakable(state, out, "![");
864                    line.write_breakable(state, out, &alt);
865                    line.write_unbreakable(state, out, &format!("]({})", x.url));
866                },
867                (true, Some(t)) => {
868                    line.write_unbreakable(state, out, "![");
869                    line.write_breakable(state, out, &alt);
870                    line.write_unbreakable(state, out, &format!("]({}", x.url));
871                    line.write_unbreakable(state, out, " \"");
872                    line.write_breakable(state, out, &join_lines(t));
873                    line.write_unbreakable(state, out, "\")");
874                },
875            }
876        },
877        Node::ImageReference(x) => {
878            line.write_unbreakable(state, out, &format!("![][{}]", x.identifier));
879        },
880        Node::Link(x) => {
881            let simple_text = if x.children.len() != 1 {
882                None
883            } else {
884                x.children.get(0)
885            }.and_then(|c| match c {
886                Node::Text(t) => {
887                    let t = join_lines(&t.value);
888                    if get_splits(&t).first().is_some() {
889                        None
890                    } else {
891                        Some(t)
892                    }
893                },
894                Node::InlineCode(t) => {
895                    let t = join_lines(&t.value);
896                    if get_splits(&t).first().is_some() {
897                        None
898                    } else {
899                        Some(format!("`{}`", t))
900                    }
901                },
902                _ => None,
903            });
904            match (simple_text, &x.title) {
905                (Some(unbroken_content), None) => {
906                    if unbroken_content.as_str() == x.url.as_str() {
907                        line.write_unbreakable(state, out, &format!("<{}>", x.url));
908                    } else {
909                        line.write_unbreakable(state, out, &format!("[{}]({})", unbroken_content, x.url));
910                    }
911                },
912                (Some(c), Some(title)) => {
913                    line.write_unbreakable(state, out, &format!("[{}]({}", c, x.url));
914                    line.write_unbreakable(state, out, " \"");
915                    line.write_breakable(state, out, title);
916                    line.write_unbreakable(state, out, "\")");
917                },
918                (None, None) => {
919                    line.write_unbreakable(state, out, "[");
920                    for child in &x.children {
921                        recurse_write(state, out, line.clone_inline(), child, true);
922                    }
923                    line.write_unbreakable(state, out, &format!("]({})", x.url));
924                },
925                (None, Some(title)) => {
926                    line.write_unbreakable(state, out, "[");
927                    for child in &x.children {
928                        recurse_write(state, out, line.clone_inline(), child, true);
929                    }
930                    line.write_unbreakable(state, out, &format!("]({}", x.url));
931                    line.write_unbreakable(state, out, " \"");
932                    line.write_breakable(state, out, title);
933                    line.write_unbreakable(state, out, "\")");
934                },
935            }
936        },
937        Node::LinkReference(x) => {
938            let simple_text = if x.children.len() != 1 {
939                None
940            } else {
941                x.children.get(0)
942            }.and_then(|c| match c {
943                Node::Text(t) => if get_splits(&t.value).first().is_some() {
944                    None
945                } else {
946                    Some(t.value.clone())
947                },
948                Node::InlineCode(t) => if get_splits(&t.value).first().is_some() {
949                    None
950                } else {
951                    Some(format!("`{}`", t.value))
952                },
953                _ => {
954                    None
955                },
956            });
957            match simple_text {
958                Some(t) if t == x.identifier => {
959                    line.write_unbreakable(state, out, &format!("[{}]", t));
960                },
961                _ => {
962                    line.write_unbreakable(state, out, "[");
963                    for child in &x.children {
964                        recurse_write(state, out, line.clone_inline(), child, true);
965                    }
966                    line.write_unbreakable(state, out, &format!("][{}]", x.identifier));
967                },
968            }
969        },
970        Node::Break(_) => {
971            // normalized out
972        },
973        Node::Math(_) => unreachable!(),
974        Node::Table(_) => unreachable!(),
975        Node::TableRow(_) => unreachable!(),
976        Node::TableCell(_) => unreachable!(),
977        Node::MdxJsxTextElement(_) => unreachable!(),
978        Node::MdxFlowExpression(_) => unreachable!(),
979        Node::MdxJsxFlowElement(_) => unreachable!(),
980        Node::MdxjsEsm(_) => unreachable!(),
981        Node::Toml(_) => unreachable!(),
982        Node::Yaml(_) => unreachable!(),
983        Node::InlineMath(_) => unreachable!(),
984        Node::MdxTextExpression(_) => unreachable!(),
985    }
986}
987
988pub fn format_md(
989    true_out: &mut String,
990    max_width: usize,
991    rel_max_width: Option<usize>,
992    prefix: &str,
993    source: &str,
994) -> Result<(), loga::Error> {
995    // TODO, due to a bug a bunch of unreachable branches might have had code added.
996    // I'd like to go back and see if some block-level starts can be removed in
997    // contexts they shouldn't appear.
998    match || -> Result<String, loga::Error> {
999        let mut out = String::new();
1000        let mut state = State {
1001            line_buffer: String::new(),
1002            need_nl: false,
1003        };
1004        let ast = markdown::to_mdast(source, &markdown::ParseOptions {
1005            constructs: markdown::Constructs { ..Default::default() },
1006            ..Default::default()
1007        }).map_err(|e| loga::err_with("Error parsing markdown", ea!(err = e)))?;
1008        recurse_write(
1009            &mut state,
1010            &mut out,
1011            LineState::new(
1012                unicode_len(&prefix),
1013                None,
1014                prefix.to_string(),
1015                VisualLen(max_width),
1016                rel_max_width.map(VisualLen),
1017                false,
1018            ),
1019            &ast,
1020            false,
1021        );
1022        Ok(out)
1023    }() {
1024        Ok(o) => {
1025            true_out.push_str(&o);
1026            Ok(())
1027        },
1028        Err(e) => {
1029            Err(e)
1030        },
1031    }
1032}