1use std::cell::Cell;
2use std::io::{self, Write};
3
4use anstyle::{AnsiColor, Effects, Style};
5
6use crate::markdown::{MdStream, MdTree};
7
8const DEFAULT_COLUMN_WIDTH: usize = 140;
9
10thread_local! {
11 static CURSOR: Cell<usize> = const { Cell::new(0) };
13 static WIDTH: Cell<usize> = const { Cell::new(DEFAULT_COLUMN_WIDTH) };
15}
16
17pub(crate) fn entrypoint(stream: &MdStream<'_>, buf: &mut Vec<u8>) -> io::Result<()> {
19 #[cfg(not(test))]
20 if let Some((w, _)) = termize::dimensions() {
21 WIDTH.set(std::cmp::min(w, DEFAULT_COLUMN_WIDTH));
22 }
23 write_stream(stream, buf, None, 0)?;
24 buf.write_all(b"\n")
25}
26fn write_stream(
28 MdStream(stream): &MdStream<'_>,
29 buf: &mut Vec<u8>,
30 default: Option<Style>,
31 indent: usize,
32) -> io::Result<()> {
33 for tt in stream {
34 write_tt(tt, buf, default, indent)?;
35 }
36 reset_opt_style(buf, default)?;
37
38 Ok(())
39}
40
41fn write_tt(
42 tt: &MdTree<'_>,
43 buf: &mut Vec<u8>,
44 default: Option<Style>,
45 indent: usize,
46) -> io::Result<()> {
47 match tt {
48 MdTree::CodeBlock { txt, lang: _ } => {
49 reset_opt_style(buf, default)?;
50 let style = Style::new().effects(Effects::DIMMED);
51 write!(buf, "{style}{txt}{style:#}")?;
52 render_opt_style(buf, default)?;
53 }
54 MdTree::CodeInline(txt) => {
55 reset_opt_style(buf, default)?;
56 write_wrapping(buf, txt, indent, None, Some(Style::new().effects(Effects::DIMMED)))?;
57 render_opt_style(buf, default)?;
58 }
59 MdTree::Strong(txt) => {
60 reset_opt_style(buf, default)?;
61 write_wrapping(buf, txt, indent, None, Some(Style::new().effects(Effects::BOLD)))?;
62 render_opt_style(buf, default)?;
63 }
64 MdTree::Emphasis(txt) => {
65 reset_opt_style(buf, default)?;
66 write_wrapping(buf, txt, indent, None, Some(Style::new().effects(Effects::ITALIC)))?;
67 render_opt_style(buf, default)?;
68 }
69 MdTree::Strikethrough(txt) => {
70 reset_opt_style(buf, default)?;
71 write_wrapping(
72 buf,
73 txt,
74 indent,
75 None,
76 Some(Style::new().effects(Effects::STRIKETHROUGH)),
77 )?;
78 render_opt_style(buf, default)?;
79 }
80 MdTree::PlainText(txt) => {
81 write_wrapping(buf, txt, indent, None, None)?;
82 }
83 MdTree::Link { disp, link } => {
84 write_wrapping(buf, disp, indent, Some(link), None)?;
85 }
86 MdTree::ParagraphBreak => {
87 buf.write_all(b"\n\n")?;
88 reset_cursor();
89 }
90 MdTree::LineBreak => {
91 buf.write_all(b"\n")?;
92 reset_cursor();
93 }
94 MdTree::HorizontalRule => {
95 (0..WIDTH.get()).for_each(|_| buf.write_all(b"-").unwrap());
96 reset_cursor();
97 }
98 MdTree::Heading(n, stream) => {
99 let cs = match n {
100 1 => AnsiColor::BrightCyan.on_default().effects(Effects::BOLD | Effects::UNDERLINE),
101 2 => AnsiColor::BrightCyan.on_default().effects(Effects::UNDERLINE),
102 3 => AnsiColor::BrightCyan.on_default().effects(Effects::ITALIC),
103 4.. => AnsiColor::Cyan.on_default().effects(Effects::UNDERLINE | Effects::ITALIC),
104 0 => unreachable!(),
105 };
106 reset_opt_style(buf, default)?;
107 write!(buf, "{cs}")?;
108 write_stream(stream, buf, Some(cs), 0)?;
109 write!(buf, "{cs:#}")?;
110 render_opt_style(buf, default)?;
111 buf.write_all(b"\n")?;
112 }
113 MdTree::OrderedListItem(n, stream) => {
114 let base = format!("{n}. ");
115 write_wrapping(buf, &format!("{base:<4}"), indent, None, None)?;
116 write_stream(stream, buf, None, indent + 4)?;
117 }
118 MdTree::UnorderedListItem(stream) => {
119 let base = "* ";
120 write_wrapping(buf, &format!("{base:<4}"), indent, None, None)?;
121 write_stream(stream, buf, None, indent + 4)?;
122 }
123 MdTree::Comment(_) | MdTree::LinkDef { .. } | MdTree::RefLink { .. } => unreachable!(),
125 }
126
127 Ok(())
128}
129
130fn render_opt_style(buf: &mut Vec<u8>, style: Option<Style>) -> io::Result<()> {
131 if let Some(style) = &style {
132 write!(buf, "{style}")?;
133 }
134 Ok(())
135}
136
137fn reset_opt_style(buf: &mut Vec<u8>, style: Option<Style>) -> io::Result<()> {
138 if let Some(style) = &style {
139 write!(buf, "{style:#}")?;
140 }
141 Ok(())
142}
143
144fn reset_cursor() {
146 CURSOR.set(0);
147}
148
149fn write_wrapping(
152 buf: &mut Vec<u8>,
153 text: &str,
154 indent: usize,
155 link_url: Option<&str>,
156 style: Option<Style>,
157) -> io::Result<()> {
158 render_opt_style(buf, style)?;
159
160 let ind_ws = &b" "[..indent];
161 let mut to_write = text;
162 if let Some(url) = link_url {
163 write!(buf, "\x1b]8;;{url}\x1b\\")?;
165 }
166 CURSOR.with(|cur| {
167 loop {
168 if cur.get() == 0 {
169 buf.write_all(ind_ws)?;
170 cur.set(indent);
171 }
172 let ch_count = WIDTH.get() - cur.get();
173 let mut iter = to_write.char_indices();
174 let Some((end_idx, _ch)) = iter.nth(ch_count) else {
175 buf.write_all(to_write.as_bytes())?;
177 cur.set(cur.get() + to_write.chars().count());
178 break;
179 };
180
181 if let Some((break_idx, ch)) = to_write[..end_idx]
182 .char_indices()
183 .rev()
184 .find(|(_idx, ch)| ch.is_whitespace() || ['_', '-'].contains(ch))
185 {
186 if ch.is_whitespace() {
188 writeln!(buf, "{}", &to_write[..break_idx])?;
189 to_write = to_write[break_idx..].trim_start();
190 } else {
191 writeln!(buf, "{}", &to_write.get(..break_idx + 1).unwrap_or(to_write))?;
193 to_write = to_write.get(break_idx + 1..).unwrap_or_default().trim_start();
194 }
195 } else {
196 let ws_idx =
198 iter.find(|(_, ch)| ch.is_whitespace()).map_or(to_write.len(), |(idx, _)| idx);
199 writeln!(buf, "{}", &to_write[..ws_idx])?;
200 to_write = to_write.get(ws_idx + 1..).map_or("", str::trim_start);
201 }
202 cur.set(0);
203 }
204 if link_url.is_some() {
205 buf.write_all(b"\x1b]8;;\x1b\\")?;
206 }
207 reset_opt_style(buf, style)?;
208 Ok(())
209 })
210}
211
212#[cfg(test)]
213#[path = "tests/term.rs"]
214mod tests;