use {
crate::{
Comment,
CommentMode,
FormatConfig,
Whitespace,
WhitespaceMode,
},
loga::{
ResultContext,
ea,
},
markdown::mdast::Node,
proc_macro2::{
Group,
LineColumn,
TokenStream,
},
regex::Regex,
std::{
cell::RefCell,
collections::BTreeMap,
hash::Hash,
rc::Rc,
str::FromStr,
},
};
pub fn extract_whitespaces(
keep_max_blank_lines: usize,
source: &str,
) -> Result<(BTreeMap<HashLineColumn, Vec<Whitespace>>, TokenStream), loga::Error> {
let mut line_lookup = vec![];
{
let mut offset = 0usize;
loop {
line_lookup.push(offset);
offset += match source[offset..].find('\n') {
Some(r) => r,
None => {
break;
},
} + 1;
}
}
struct State<'a> {
block_event_re: Option<Regex>,
keep_max_blank_lines: usize,
last_offset: usize,
line_lookup: Vec<usize>,
line_start: Option<LineColumn>,
source: &'a str,
start_re: Option<Regex>,
whitespaces: BTreeMap<HashLineColumn, Vec<Whitespace>>,
}
impl<'a> State<'a> {
fn add_comments(&mut self, end: LineColumn, abs_start: usize, between_ast_nodes: &str) {
let start_re = &self.start_re.get_or_insert_with(|| Regex::new(
r#"(?:(//)(/|!|\.|\?|#)?)|(/\*\*/)|(?:(/\*)(\*|!)?)"#,
).unwrap());
let block_event_re =
&self.block_event_re.get_or_insert_with(|| Regex::new(r#"((?:/\*)|(?:\*/))"#).unwrap());
struct CommentBuffer {
blank_lines: usize,
keep_max_blank_lines: usize,
lines: Vec<String>,
loc: LineColumn,
mode: CommentMode,
orig_start_offset: Option<usize>,
out: Vec<Whitespace>,
}
impl CommentBuffer {
fn add(&mut self, mode: CommentMode, line: &str, orig_start_offset: usize) {
if self.mode != mode && !self.lines.is_empty() {
self.flush();
}
self.mode = mode;
self.lines.push(line.to_string());
self.orig_start_offset.get_or_insert(orig_start_offset);
}
fn add_blank_lines(&mut self, text: &str) {
let blank_lines = text.as_bytes().iter().filter(|x| **x == b'\n').count();
if blank_lines > 1 && self.keep_max_blank_lines > 0 {
self.flush();
self.out.push(Whitespace {
loc: self.loc,
mode: crate::WhitespaceMode::BlankLines(
(blank_lines - 1).min(self.keep_max_blank_lines),
),
});
}
}
fn flush(&mut self) {
if self.lines.is_empty() {
return;
}
self.out.push(Whitespace {
loc: self.loc,
mode: crate::WhitespaceMode::Comment(Comment {
mode: self.mode,
lines: self.lines.split_off(0).join("\n"),
orig_start_offset: self.orig_start_offset.unwrap(),
}),
});
self.blank_lines = 0;
self.orig_start_offset = None;
}
}
let mut buffer = CommentBuffer {
keep_max_blank_lines: self.keep_max_blank_lines,
blank_lines: 0,
out: vec![],
mode: CommentMode::Normal,
lines: vec![],
loc: end,
orig_start_offset: None,
};
let mut text = (abs_start, between_ast_nodes);
'comment_loop : loop {
match start_re.captures(text.1) {
Some(found_start) => {
let orig_start_offset = abs_start + found_start.get(0).unwrap().start();
let start_prefix_match =
found_start
.get(1)
.or_else(|| found_start.get(3))
.or_else(|| found_start.get(4))
.unwrap();
if buffer.out.is_empty() && buffer.lines.is_empty() {
buffer.add_blank_lines(&text.1[..start_prefix_match.start()]);
}
match start_prefix_match.as_str() {
"//" => {
let mode = {
let start_suffix_match = found_start.get(2);
let (mut mode, mut match_end) = match start_suffix_match {
Some(start_suffix_match) => (match start_suffix_match.as_str() {
"/" => CommentMode::DocOuter,
"!" => CommentMode::DocInner,
"." => CommentMode::Verbatim,
"#" => CommentMode::Directive,
"?" => CommentMode::ExplicitNormal,
_ => unreachable!(),
}, start_suffix_match.end()),
None => (CommentMode::Normal, start_prefix_match.end()),
};
if mode == CommentMode::DocOuter && text.1[match_end..].starts_with("/") {
mode = CommentMode::Normal;
match_end = start_prefix_match.end();
}
text = (text.0 + match_end, &text.1[match_end..]);
mode
};
let (line, next_start) = match text.1.find('\n') {
Some(line_end) => (&text.1[..line_end], line_end + 1),
None => (text.1, text.1.len()),
};
buffer.add(mode, line, orig_start_offset);
text = (text.0 + next_start, &text.1[next_start..]);
},
"/**/" => {
buffer.add(CommentMode::Normal, "".into(), orig_start_offset);
text = (text.0 + start_prefix_match.end(), &text.1[start_prefix_match.end()..]);
},
"/*" => {
let mode = {
let start_suffix_match = found_start.get(5);
let (mode, match_end) = match start_suffix_match {
Some(start_suffix_match) => (match start_suffix_match.as_str() {
"*" => CommentMode::DocOuter,
"!" => CommentMode::DocInner,
_ => unreachable!(),
}, start_suffix_match.end()),
None => (CommentMode::Normal, start_prefix_match.end()),
};
text = (text.0 + match_end, &text.1[match_end..]);
mode
};
let mut nesting = 1;
let mut search_end_at = 0usize;
let (lines, next_start) = loop {
let found_event =
block_event_re.captures(&text.1[search_end_at..]).unwrap().get(1).unwrap();
let event_start = search_end_at + found_event.start();
search_end_at += found_event.end();
match found_event.as_str() {
"/*" => {
nesting += 1;
},
"*/" => {
nesting -= 1;
if nesting == 0 {
break (&text.1[..event_start], search_end_at);
}
},
_ => unreachable!(),
}
};
for line in lines.lines() {
let mut line = line.trim();
line = line.strip_prefix("* ").unwrap_or(line);
buffer.add(mode, line, orig_start_offset);
}
text = (text.0 + next_start, &text.1[next_start..]);
},
_ => unreachable!(),
}
},
None => {
if buffer.out.is_empty() && buffer.lines.is_empty() {
buffer.add_blank_lines(text.1);
}
break 'comment_loop;
},
}
}
buffer.flush();
if !buffer.out.is_empty() {
let whitespaces = self.whitespaces.entry(HashLineColumn(end)).or_insert(vec![]);
'merge : loop {
let Some(previous_whitespace) = whitespaces.last_mut() else {
break;
};
let WhitespaceMode::Comment(previous_comment) = &mut previous_whitespace.mode else {
break;
};
let start = buffer.out.remove(0);
loop {
let WhitespaceMode::Comment(start_comment) = &start.mode else {
break;
};
if previous_comment.mode != start_comment.mode {
break;
}
previous_comment.lines.push_str("\n");
previous_comment.lines.push_str(&start_comment.lines);
break 'merge;
}
buffer.out.insert(0, start);
break;
}
whitespaces.extend(buffer.out);
}
}
fn extract(&mut self, mut start: usize, end: LineColumn) {
if loop {
let previous_start = match &self.line_start {
None => break true,
Some(s) => s,
};
if end.line <= previous_start.line {
break false;
}
let eol = match self.source[start..].find('\n') {
Some(n) => start + n,
None => self.source.len(),
};
let text = &self.source[start .. eol];
if text.trim_start().starts_with("//") {
self.add_comments(*previous_start, start, text);
}
start = eol;
break true;
} {
self.line_start = Some(end);
}
let end_offset = self.to_offset(end);
if end_offset < start {
return;
}
let whole_text = &self.source[start .. end_offset];
self.add_comments(end, start, whole_text);
}
fn to_offset(&self, loc: LineColumn) -> usize {
if loc.line == 0 {
return 0usize;
}
let line_start_offset = *self.line_lookup.get(loc.line - 1).unwrap();
line_start_offset +
self.source[line_start_offset..].chars().take(loc.column).map(char::len_utf8).sum::<usize>()
}
}
let mut state = State {
source: source,
keep_max_blank_lines: keep_max_blank_lines,
line_lookup: line_lookup,
whitespaces: BTreeMap::new(),
last_offset: 0usize,
line_start: None,
start_re: None,
block_event_re: None,
};
fn recurse(state: &mut State, ts: TokenStream) -> TokenStream {
let mut out = vec![];
let mut ts = ts.into_iter().peekable();
while let Some(t) = ts.next() {
match t {
proc_macro2::TokenTree::Group(g) => {
state.extract(state.last_offset, g.span_open().start());
state.last_offset = state.to_offset(g.span_open().end());
let subtokens = recurse(state, g.stream());
state.extract(state.last_offset, g.span_close().start());
state.last_offset = state.to_offset(g.span_close().end());
let mut new_g = Group::new(g.delimiter(), subtokens);
new_g.set_span(g.span());
out.push(proc_macro2::TokenTree::Group(new_g));
},
proc_macro2::TokenTree::Ident(g) => {
state.extract(state.last_offset, g.span().start());
state.last_offset = state.to_offset(g.span().end());
out.push(proc_macro2::TokenTree::Ident(g));
},
proc_macro2::TokenTree::Punct(g) => {
let offset = state.to_offset(g.span().start());
if g.as_char() == '#' && &state.source[offset .. offset + 1] == "/" {
loop {
let in_comment = ts.peek().map(|n| n.span().start() < g.span().end()).unwrap_or(false);
if !in_comment {
break;
}
ts.next();
}
} else {
state.extract(state.last_offset, g.span().start());
state.last_offset = state.to_offset(g.span().end());
out.push(proc_macro2::TokenTree::Punct(g));
}
},
proc_macro2::TokenTree::Literal(g) => {
state.extract(state.last_offset, g.span().start());
state.last_offset = state.to_offset(g.span().end());
out.push(proc_macro2::TokenTree::Literal(g));
},
}
}
TokenStream::from_iter(out)
}
let tokens =
recurse(
&mut state,
TokenStream::from_str(
source,
).map_err(
|e| loga::err_with(
"Error undoing syn parse transformations",
ea!(
line = e.span().start().line,
column = e.span().start().column,
error = e.to_string(),
source = source.lines().skip(e.span().start().line - 1).next().unwrap()
),
),
)?,
);
state.add_comments(LineColumn {
line: 0,
column: 1,
}, state.last_offset, &source[state.last_offset..]);
Ok((state.whitespaces, tokens))
}
pub fn format_md(
true_out: &mut String,
config: &FormatConfig,
prefix: &str,
source: &str,
) -> Result<(), loga::Error> {
match || -> Result<String, loga::Error> {
let mut out = String::new();
let mut state = State {
line_buffer: String::new(),
need_nl: false,
config: config.clone(),
};
let ast = markdown::to_mdast(source, &markdown::ParseOptions {
constructs: markdown::Constructs { ..Default::default() },
..Default::default()
}).map_err(loga::err).context("Error parsing markdown")?;
recurse_write(
&mut state,
&mut out,
LineState::new(
unicode_len(&prefix),
None,
prefix.to_string(),
VisualLen(config.max_width),
config.comment_width.map(VisualLen),
),
&ast,
false,
);
Ok(out)
}() {
Ok(o) => {
true_out.push_str(&o);
Ok(())
},
Err(e) => {
Err(e)
},
}
}
fn get_splits(text: &str) -> Vec<usize> {
text.char_indices().filter(|i| i.1 == ' ').map(|i| i.0 + 1).collect()
}
#[derive(PartialEq, Eq, Debug, Clone, Copy)]
pub struct HashLineColumn(pub LineColumn);
impl Hash for HashLineColumn {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
(self.0.line, self.0.column).hash(state);
}
}
impl Ord for HashLineColumn {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
return self.0.line.cmp(&other.0.line).then(self.0.column.cmp(&other.0.column));
}
}
impl PartialOrd for HashLineColumn {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
return Some(self.0.cmp(&other.0));
}
}
struct LineState(Rc<RefCell<LineState_>>);
impl LineState {
fn clone_indent(&self, first_prefix: Option<String>, prefix: String) -> LineState {
let mut s = self.0.as_ref().borrow_mut();
LineState(Rc::new(RefCell::new(LineState_ {
base_prefix_len: s.base_prefix_len,
first_prefix: match (s.first_prefix.take(), first_prefix) {
(None, None) => None,
(None, Some(p)) => Some(format!("{}{}", s.prefix, p)),
(Some(p), None) => Some(p),
(Some(p1), Some(p2)) => Some(format!("{}{}", p1, p2)),
},
prefix: format!("{}{}", s.prefix, prefix),
max_width: s.max_width,
rel_max_width: s.rel_max_width,
backward_break: None,
unbreakable: false,
})))
}
fn clone_inline(&self) -> LineState {
LineState(self.0.clone())
}
fn clone_unbreakable(&self, first_prefix: Option<String>) -> LineState {
let mut s = self.0.as_ref().borrow_mut();
LineState(Rc::new(RefCell::new(LineState_ {
base_prefix_len: s.base_prefix_len,
first_prefix: match (s.first_prefix.take(), first_prefix) {
(None, None) => None,
(None, Some(p)) => Some(format!("{}{}", s.prefix, p)),
(Some(p), None) => Some(p),
(Some(p1), Some(p2)) => Some(format!("{}{}", p1, p2)),
},
prefix: s.prefix.clone(),
max_width: s.max_width,
rel_max_width: s.rel_max_width,
backward_break: None,
unbreakable: true,
})))
}
fn clone_zero_indent(&self) -> LineState {
let mut s = self.0.as_ref().borrow_mut();
LineState(Rc::new(RefCell::new(LineState_ {
base_prefix_len: s.base_prefix_len,
first_prefix: s.first_prefix.take(),
prefix: s.prefix.clone(),
max_width: s.max_width,
rel_max_width: s.rel_max_width,
backward_break: None,
unbreakable: false,
})))
}
fn flush_always(&self, state: &mut State, out: &mut String) {
self.0.as_ref().borrow_mut().flush_always(state, out);
}
fn new(
base_prefix_len: VisualLen,
first_prefix: Option<String>,
prefix: String,
max_width: VisualLen,
rel_max_width: Option<VisualLen>,
) -> LineState {
LineState(Rc::new(RefCell::new(LineState_ {
base_prefix_len: base_prefix_len,
first_prefix,
prefix,
max_width,
rel_max_width,
backward_break: None,
unbreakable: false,
})))
}
fn write(&self, state: &mut State, out: &mut String, text: &str, breaks: &[usize]) {
let mut s = self.0.as_ref().borrow_mut();
if s.unbreakable {
state.line_buffer.push_str(text);
return;
}
let max_len = s.calc_max_width();
struct FoundWritableLen<'a> {
next_break: Option<(usize, &'a [usize])>,
previous_break: Option<(usize, &'a [usize])>,
writable: usize,
}
fn find_writable_len<
'a,
>(
width: VisualLen,
max_len: VisualLen,
text: &str,
breaks_offset: usize,
breaks: &'a [usize],
) -> FoundWritableLen<'a> {
let mut previous_break = None;
let mut writable = 0;
for (i, b) in breaks.iter().enumerate() {
let b = *b - breaks_offset;
let next_break = Some((b, &breaks[i + 1..]));
if width + unicode_len(&text[..b]) > max_len {
return FoundWritableLen {
writable: writable,
previous_break: previous_break,
next_break: next_break,
};
}
previous_break = next_break;
writable = b;
}
return FoundWritableLen {
writable: if width + unicode_len(&text) > max_len {
writable
} else {
text.len()
},
previous_break: previous_break,
next_break: None,
};
}
fn write_forward(state: &mut State, s: &mut LineState_, text: &str, b: Option<usize>) {
if let Some(b) = b {
s.backward_break = Some(state.line_buffer.len() + b);
}
state.line_buffer.push_str(&text);
}
fn write_forward_breaks(
state: &mut State,
s: &mut LineState_,
out: &mut String,
max_len: VisualLen,
mut first: bool,
mut text: String,
mut breaks_offset: usize,
breaks: &[usize],
) {
let mut breaks = breaks;
while !text.is_empty() {
if first {
first = false;
} else {
s.flush(state, out);
}
let found = find_writable_len(s.calc_current_len(state), max_len, &text, breaks_offset, breaks);
if found.writable > 0 {
write_forward(state, s, &text[..found.writable], found.previous_break.map(|b| b.0));
breaks = found.previous_break.map(|b| b.1).unwrap_or(breaks);
text = text.split_off(found.writable);
breaks_offset += found.writable;
} else if let Some((b, breaks0)) = found.next_break {
write_forward(state, s, &text[..b], Some(b));
breaks = breaks0;
text = text.split_off(b);
breaks_offset += b;
} else {
state.line_buffer.push_str(&text);
return;
}
}
}
let found = find_writable_len(s.calc_current_len(state), max_len, text, 0, breaks);
if found.writable > 0 {
write_forward(state, &mut s, &text[..found.writable], found.previous_break.map(|b| b.0));
write_forward_breaks(
state,
&mut s,
out,
max_len,
false,
(&text[found.writable..]).to_string(),
found.writable,
found.previous_break.map(|b| b.1).unwrap_or(breaks),
);
} else if let Some(at) = s.backward_break.take() {
let prefix = state.line_buffer.split_off(at);
s.flush(state, out);
state.line_buffer.push_str(&prefix);
write_forward_breaks(state, &mut s, out, max_len, true, text.to_string(), 0, breaks);
} else if let Some((b, breaks)) = found.next_break {
write_forward(state, &mut s, &text[..b], Some(b));
write_forward_breaks(state, &mut s, out, max_len, false, (&text[b..]).to_string(), b, breaks);
} else {
state.line_buffer.push_str(text);
}
}
fn write_breakable(&self, state: &mut State, out: &mut String, text: &str) {
self.write(state, out, text, &get_splits(text));
}
fn write_newline(&self, state: &mut State, out: &mut String) {
let mut s = self.0.as_ref().borrow_mut();
if !state.line_buffer.is_empty() {
panic!();
}
s.flush_always(state, out);
}
fn write_unbreakable(&self, state: &mut State, out: &mut String, text: &str) {
self.write(state, out, text, &[]);
}
}
#[derive(Debug)]
struct LineState_ {
backward_break: Option<usize>,
base_prefix_len: VisualLen,
first_prefix: Option<String>,
max_width: VisualLen,
prefix: String,
rel_max_width: Option<VisualLen>,
unbreakable: bool,
}
impl LineState_ {
fn calc_current_len(&self, state: &State) -> VisualLen {
self.base_prefix_len + unicode_len(&state.line_buffer)
}
fn calc_max_width(&self) -> VisualLen {
match self.rel_max_width {
Some(w) => unicode_len(&self.prefix) + w,
None => self.max_width,
}
}
fn flush(&mut self, state: &mut State, out: &mut String) {
if !state.line_buffer.trim().is_empty() {
self.flush_always(state, out);
}
}
fn flush_always(&mut self, state: &mut State, out: &mut String) {
out.push_str(format!(
"{}{}{}",
if state.need_nl {
"\n"
} else {
""
},
match &self.first_prefix.take() {
Some(t) => t,
None => &*self.prefix,
},
&state.line_buffer,
).trim_end());
state.line_buffer.clear();
state.need_nl = true;
self.backward_break = None;
}
}
fn recurse_write(state: &mut State, out: &mut String, line: LineState, node: &Node, inline: bool) {
fn join_lines(text: &str) -> String {
let lines = Regex::new("\r?\n").unwrap().split(text).collect::<Vec<&str>>();
let mut joined = String::new();
for (i, line) in lines.iter().enumerate() {
let mut line = *line;
if i > 0 {
line = line.trim_start();
joined.push(' ');
}
if i < lines.len() - 1 {
line = line.trim_end();
}
joined.push_str(line);
}
joined
}
match node {
Node::Root(x) => {
for (i, child) in x.children.iter().enumerate() {
if i > 0 {
line.write_newline(state, out);
}
recurse_write(state, out, line.clone_zero_indent(), child, false);
}
},
Node::Blockquote(x) => {
let line = line.clone_indent(None, "> ".into());
for (i, child) in x.children.iter().enumerate() {
if i > 0 {
line.write_newline(state, out);
}
recurse_write(state, out, line.clone_inline(), child, false);
}
},
Node::List(x) => {
match &x.start {
Some(i) => {
for (j, child) in x.children.iter().enumerate() {
if j > 0 {
line.write_newline(state, out);
}
recurse_write(
state,
out,
line.clone_indent(Some(format!("{}. ", *i as usize + j)), " ".into()),
child,
false,
);
}
},
None => {
for (i, child) in x.children.iter().enumerate() {
if i > 0 {
line.write_newline(state, out);
}
recurse_write(state, out, line.clone_indent(Some("* ".into()), " ".into()), child, false);
}
},
};
},
Node::ListItem(x) => {
for (i, child) in x.children.iter().enumerate() {
if i > 0 {
line.write_newline(state, out);
}
recurse_write(state, out, line.clone_zero_indent(), child, false);
}
},
Node::Code(x) => {
let mut content = None;
if let Some(lang) = &x.lang {
if lang == "rust" {
let current_prefix_len = line.0.borrow().prefix.chars().count();
let base_prefix_len = line.0.borrow().base_prefix_len.0;
let rel_max_width = line.0.borrow().rel_max_width.map(|v| v.0);
let overall_max_width = line.0.borrow().max_width.0;
let nested_max_width = if let Some(rel) = rel_max_width {
let markdown_indent = current_prefix_len - base_prefix_len;
rel.saturating_sub(markdown_indent)
} else {
overall_max_width.saturating_sub(current_prefix_len)
};
let nested_max_width = std::cmp::max(nested_max_width, 40);
let mut nested_config = state.config.clone();
nested_config.max_width = nested_max_width;
match crate::format_str(&x.value, &nested_config) {
Ok(res) => {
content = Some(res.rendered);
},
Err(_) => {
},
}
} else if let Some(fmt_config) = state.config.external_formatters.get(lang.as_str()).cloned() {
match crate::run_external_formatter(&fmt_config.commandline, &x.value) {
Ok(formatted) => {
content = Some(formatted);
},
Err(_) => {
},
}
}
}
line.write_unbreakable(state, out, &format!("```{}", match &x.lang {
None => "",
Some(x) => x,
}));
line.flush_always(state, out);
let content = content.unwrap_or_else(|| x.value.clone());
for l in content.as_str().lines() {
line.write_unbreakable(state, out, l);
line.flush_always(state, out);
}
line.write_unbreakable(state, out, "```");
line.flush_always(state, out);
},
Node::Heading(x) => {
let line = line.clone_unbreakable(Some(format!("{} ", "#".repeat(x.depth as usize))));
for child in &x.children {
recurse_write(state, out, line.clone_inline(), child, true);
}
line.flush_always(state, out);
},
Node::FootnoteDefinition(x) => {
let line = line.clone_indent(Some(format!("[^{}]: ", x.identifier)), " ".into());
for child in &x.children {
recurse_write(state, out, line.clone_inline(), child, true);
}
line.flush_always(state, out);
},
Node::ThematicBreak(_) => {
line.write_unbreakable(state, out, "---");
line.flush_always(state, out);
},
Node::Definition(x) => {
line.write_unbreakable(state, out, &format!("[{}]: {}", x.identifier.trim(), x.url));
if let Some(title) = &x.title {
line.write_unbreakable(state, out, " \"");
line.write_breakable(state, out, title);
line.write_unbreakable(state, out, "\"");
}
line.flush_always(state, out);
},
Node::Paragraph(x) => {
for child in &x.children {
recurse_write(state, out, line.clone_inline(), child, true);
}
line.flush_always(state, out);
},
Node::Html(x) if !inline => {
line.write_unbreakable(state, out, &format!("`{}`", join_lines(&x.value)));
line.flush_always(state, out);
},
Node::Text(x) => {
line.write_breakable(state, out, &join_lines(&x.value));
},
Node::InlineCode(x) => {
line.write_unbreakable(state, out, &format!("`{}`", join_lines(&x.value)));
},
Node::Strong(x) => {
line.write_unbreakable(state, out, "**");
for child in &x.children {
recurse_write(state, out, line.clone_inline(), child, true);
}
line.write_unbreakable(state, out, "**");
},
Node::Delete(x) => {
line.write_unbreakable(state, out, "~~");
for child in &x.children {
recurse_write(state, out, line.clone_inline(), child, true);
}
line.write_unbreakable(state, out, "~~");
},
Node::Emphasis(x) => {
line.write_unbreakable(state, out, "_");
for child in &x.children {
recurse_write(state, out, line.clone_inline(), child, true);
}
line.write_unbreakable(state, out, "_");
},
Node::FootnoteReference(x) => {
line.write_unbreakable(state, out, &format!("[^{}]", x.identifier));
},
Node::Html(x) => {
line.write_unbreakable(state, out, &format!("`{}`", join_lines(&x.value)));
},
Node::Image(x) => {
let alt = join_lines(&x.alt);
match (get_splits(&join_lines(&alt)).first().is_some(), &x.title) {
(false, None) => {
line.write_unbreakable(state, out, &format!("", alt, x.url));
},
(false, Some(t)) => {
line.write_unbreakable(state, out, &format!(");
line.write_unbreakable(state, out, " \"");
line.write_breakable(state, out, &join_lines(t));
line.write_unbreakable(state, out, "\")");
},
(true, None) => {
line.write_unbreakable(state, out, "", x.url));
},
(true, Some(t)) => {
line.write_unbreakable(state, out, ");
line.write_unbreakable(state, out, " \"");
line.write_breakable(state, out, &join_lines(t));
line.write_unbreakable(state, out, "\")");
},
}
},
Node::ImageReference(x) => {
line.write_unbreakable(state, out, &format!("![][{}]", x.identifier));
},
Node::Link(x) => {
let simple_text = if x.children.len() != 1 {
None
} else {
x.children.get(0)
}.and_then(|c| match c {
Node::Text(t) => {
let t = join_lines(&t.value);
if get_splits(&t).first().is_some() {
None
} else {
Some(t)
}
},
Node::InlineCode(t) => {
let t = join_lines(&t.value);
if get_splits(&t).first().is_some() {
None
} else {
Some(format!("`{}`", t))
}
},
_ => None,
});
match (simple_text, &x.title) {
(Some(unbroken_content), None) => {
if unbroken_content.as_str() == x.url.as_str() {
line.write_unbreakable(state, out, &format!("<{}>", x.url));
} else {
line.write_unbreakable(state, out, &format!("[{}]({})", unbroken_content, x.url));
}
},
(Some(c), Some(title)) => {
line.write_unbreakable(state, out, &format!("[{}]({}", c, x.url));
line.write_unbreakable(state, out, " \"");
line.write_breakable(state, out, title);
line.write_unbreakable(state, out, "\")");
},
(None, None) => {
line.write_unbreakable(state, out, "[");
for child in &x.children {
recurse_write(state, out, line.clone_inline(), child, true);
}
line.write_unbreakable(state, out, &format!("]({})", x.url));
},
(None, Some(title)) => {
line.write_unbreakable(state, out, "[");
for child in &x.children {
recurse_write(state, out, line.clone_inline(), child, true);
}
line.write_unbreakable(state, out, &format!("]({}", x.url));
line.write_unbreakable(state, out, " \"");
line.write_breakable(state, out, title);
line.write_unbreakable(state, out, "\")");
},
}
},
Node::LinkReference(x) => {
let simple_text = if x.children.len() != 1 {
None
} else {
x.children.get(0)
}.and_then(|c| match c {
Node::Text(t) => if get_splits(&t.value).first().is_some() {
None
} else {
Some(t.value.clone())
},
Node::InlineCode(t) => if get_splits(&t.value).first().is_some() {
None
} else {
Some(format!("`{}`", t.value))
},
_ => {
None
},
});
match simple_text {
Some(t) if t == x.identifier => {
line.write_unbreakable(state, out, &format!("[{}]", t));
},
_ => {
line.write_unbreakable(state, out, "[");
for child in &x.children {
recurse_write(state, out, line.clone_inline(), child, true);
}
line.write_unbreakable(state, out, &format!("][{}]", x.identifier));
},
}
},
Node::Break(_) => {
},
Node::Math(_) => unreachable!(),
Node::Table(_) => unreachable!(),
Node::TableRow(_) => unreachable!(),
Node::TableCell(_) => unreachable!(),
Node::MdxJsxTextElement(_) => unreachable!(),
Node::MdxFlowExpression(_) => unreachable!(),
Node::MdxJsxFlowElement(_) => unreachable!(),
Node::MdxjsEsm(_) => unreachable!(),
Node::Toml(_) => unreachable!(),
Node::Yaml(_) => unreachable!(),
Node::InlineMath(_) => unreachable!(),
Node::MdxTextExpression(_) => unreachable!(),
}
}
struct State {
config: FormatConfig,
line_buffer: String,
need_nl: bool,
}
fn unicode_len(text: &str) -> VisualLen {
VisualLen(text.chars().count())
}
#[derive(Debug, derive_more::Add, PartialEq, Eq, PartialOrd, Ord, derive_more::Sub, Clone, Copy)]
struct VisualLen(usize);