1use crate::util::{header_text, unwrap};
4use crate::EventIter;
5use anyhow::{bail, Error};
6use pulldown_cmark::{Alignment, Event, HeadingLevel, LinkType, Tag, TagEnd};
7use std::fmt::Write;
8use std::mem;
9use url::Url;
10
11pub struct TextFormatter {
12 url: Option<Url>,
13}
14
15impl TextFormatter {
16 pub fn new(url: Option<Url>) -> TextFormatter {
17 TextFormatter { url }
18 }
19}
20
21impl super::Formatter for TextFormatter {
22 fn render(&self, input: &str) -> Result<String, Error> {
23 TextRenderer::render(input, self.url.clone(), 0)
24 }
25
26 fn render_options_start(&self) -> &'static str {
27 "<![CDATA[\n"
30 }
31
32 fn render_options_end(&self) -> &'static str {
33 "]]>\n"
34 }
35
36 fn render_option(
37 &self,
38 params: &[&str],
39 block: &str,
40 _man_name: &str,
41 ) -> Result<String, Error> {
42 let rendered_options = params
43 .iter()
44 .map(|param| TextRenderer::render(param, self.url.clone(), 0))
45 .collect::<Result<Vec<_>, Error>>()?;
46 let trimmed: Vec<_> = rendered_options.iter().map(|o| o.trim()).collect();
47 Ok(format!(
49 "<dt>{}</dt>\n<dd>\n{}</dd>\n<br>\n",
50 trimmed.join(", "),
51 block
52 ))
53 }
54
55 fn linkify_man_to_md(&self, name: &str, section: u8) -> Result<String, Error> {
56 Ok(format!("`{}`({})", name, section))
57 }
58}
59
60struct TextRenderer<'e> {
61 output: String,
62 indent: usize,
63 line: String,
67 word: String,
70 parser: EventIter<'e>,
71 url: Option<Url>,
73 table: Table,
74}
75
76impl<'e> TextRenderer<'e> {
77 fn render(input: &str, url: Option<Url>, indent: usize) -> Result<String, Error> {
78 let parser = crate::md_parser(input, url.clone());
79 let output = String::with_capacity(input.len() * 3 / 2);
80 let mut mr = TextRenderer {
81 output,
82 indent,
83 line: String::new(),
84 word: String::new(),
85 parser,
86 url,
87 table: Table::new(),
88 };
89 mr.push_md()?;
90 Ok(mr.output)
91 }
92
93 fn push_md(&mut self) -> Result<(), Error> {
94 let mut in_cdata = false;
97 let mut list: Vec<Option<u64>> = Vec::new();
100 let mut suppress_paragraph = false;
102 let mut wrap_text = true;
104
105 let mut last_seen_link_data = None;
106 while let Some((event, range)) = self.parser.next() {
107 let this_suppress_paragraph = suppress_paragraph;
108 suppress_paragraph = false;
112 match event {
113 Event::Start(tag) => {
114 match tag {
115 Tag::Paragraph => {
116 if !this_suppress_paragraph {
117 self.flush();
118 }
119 }
120 Tag::Heading { level, .. } => {
121 self.flush();
122 if level == HeadingLevel::H1 {
123 let text = header_text(&mut self.parser)?;
124 self.push_to_line(&text.to_uppercase());
125 self.hard_break();
126 self.hard_break();
127 } else if level == HeadingLevel::H2 {
128 let text = header_text(&mut self.parser)?;
129 self.push_to_line(&text.to_uppercase());
130 self.flush();
131 self.indent = 7;
132 } else {
133 let text = header_text(&mut self.parser)?;
134 self.push_indent((level as usize - 2) * 3);
135 self.push_to_line(&text);
136 self.flush();
137 self.indent = (level as usize - 1) * 3 + 1;
138 }
139 }
140 Tag::BlockQuote(_kind) => {
141 self.indent += 3;
142 }
143 Tag::CodeBlock(_kind) => {
144 self.flush();
145 wrap_text = false;
146 self.indent += 4;
147 }
148 Tag::List(start) => list.push(start),
149 Tag::Item => {
150 self.flush();
151 match list.last_mut().expect("item must have list start") {
152 Some(n) => {
154 self.push_indent(self.indent);
155 write!(self.line, "{}.", n)?;
156 *n += 1;
157 }
158 None => {
160 self.push_indent(self.indent);
161 self.push_to_line("o ")
162 }
163 }
164 self.indent += 3;
165 suppress_paragraph = true;
166 }
167 Tag::FootnoteDefinition(_label) => unimplemented!(),
168 Tag::Table(alignment) => {
169 assert!(self.table.alignment.is_empty());
170 self.flush();
171 self.table.alignment.extend(alignment);
172 let table = self.table.process(&mut self.parser, self.indent)?;
173 self.output.push_str(&table);
174 self.hard_break();
175 self.table = Table::new();
176 }
177 Tag::TableHead | Tag::TableRow | Tag::TableCell => {
178 bail!("unexpected table element")
179 }
180 Tag::Emphasis => {}
181 Tag::Strong => {}
182 Tag::Strikethrough => self.word.push_str("~~"),
184 Tag::Link {
185 link_type,
186 dest_url,
187 ..
188 } => {
189 last_seen_link_data = Some((link_type.clone(), dest_url.to_owned()));
190 if dest_url.starts_with('#') {
191 continue;
194 }
195 match link_type {
196 LinkType::Autolink | LinkType::Email => {
197 match self.parser.next() {
199 Some((Event::Text(_), _range)) => {}
200 _ => bail!("expected text after autolink"),
201 }
202 }
203 LinkType::Inline
204 | LinkType::Reference
205 | LinkType::Collapsed
206 | LinkType::Shortcut => {}
207 LinkType::ReferenceUnknown
212 | LinkType::CollapsedUnknown
213 | LinkType::ShortcutUnknown => {
214 bail!(
215 "link with missing reference `{}` located at offset {}",
216 dest_url,
217 range.start
218 );
219 }
220 LinkType::WikiLink { .. } => {
221 panic!("wikilink unsupported");
222 }
223 }
224 }
225 Tag::Image { .. } => {
226 bail!("images are not currently supported")
227 }
228 Tag::HtmlBlock { .. }
229 | Tag::MetadataBlock { .. }
230 | Tag::DefinitionList
231 | Tag::DefinitionListTitle
232 | Tag::DefinitionListDefinition
233 | Tag::Superscript
234 | Tag::Subscript => {}
235 }
236 }
237 Event::End(tag_end) => match &tag_end {
238 TagEnd::Paragraph => {
239 self.flush();
240 self.hard_break();
241 }
242 TagEnd::Heading(..) => {}
243 TagEnd::BlockQuote(..) => {
244 self.indent -= 3;
245 }
246 TagEnd::CodeBlock => {
247 self.hard_break();
248 wrap_text = true;
249 self.indent -= 4;
250 }
251 TagEnd::List(..) => {
252 list.pop();
253 }
254 TagEnd::Item => {
255 self.flush();
256 self.indent -= 3;
257 self.hard_break();
258 }
259 TagEnd::FootnoteDefinition => {}
260 TagEnd::Table => {}
261 TagEnd::TableHead => {}
262 TagEnd::TableRow => {}
263 TagEnd::TableCell => {}
264 TagEnd::Emphasis => {}
265 TagEnd::Strong => {}
266 TagEnd::Strikethrough => self.word.push_str("~~"),
267 TagEnd::Link => {
268 if let Some((link_type, ref dest_url)) = last_seen_link_data {
269 if dest_url.starts_with('#') {
270 continue;
271 }
272 match link_type {
273 LinkType::Autolink | LinkType::Email => {}
274 LinkType::Inline
275 | LinkType::Reference
276 | LinkType::Collapsed
277 | LinkType::Shortcut => self.flush_word(),
278 _ => {
279 panic!("unexpected tag {:?}", tag_end);
280 }
281 }
282 self.flush_word();
283 write!(self.word, "<{}>", dest_url)?;
284 }
285 }
286 TagEnd::HtmlBlock { .. }
287 | TagEnd::MetadataBlock { .. }
288 | TagEnd::DefinitionList
289 | TagEnd::DefinitionListTitle
290 | TagEnd::Image
291 | TagEnd::DefinitionListDefinition
292 | TagEnd::Superscript
293 | TagEnd::Subscript => {}
294 },
295 Event::Text(t) | Event::Code(t) => {
296 if wrap_text {
297 let chunks = split_chunks(&t);
298 for chunk in chunks {
299 if chunk == " " {
300 self.flush_word();
301 } else {
302 self.word.push_str(chunk);
303 }
304 }
305 } else {
306 for line in t.lines() {
307 self.push_indent(self.indent);
308 self.push_to_line(line);
309 self.flush();
310 }
311 }
312 }
313 Event::Html(t) => {
314 if t.starts_with("<![CDATA[") {
315 in_cdata = true;
317 self.flush();
318 } else if in_cdata {
319 if t.trim().ends_with("]]>") {
320 in_cdata = false;
321 } else {
322 let trimmed = t.trim();
323 if trimmed.is_empty() {
324 continue;
325 }
326 if trimmed == "<br>" {
327 self.hard_break();
328 } else if trimmed.starts_with("<dt>") {
329 let opts = unwrap(trimmed, "<dt>", "</dt>");
330 self.push_indent(self.indent);
331 self.push_to_line(opts);
332 self.flush();
333 } else if trimmed.starts_with("<dd>") {
334 let mut def = String::new();
335 while let Some((Event::Html(t), _range)) = self.parser.next() {
336 if t.starts_with("</dd>") {
337 break;
338 }
339 def.push_str(&t);
340 }
341 let rendered =
342 TextRenderer::render(&def, self.url.clone(), self.indent + 4)?;
343 self.push_to_line(rendered.trim_end());
344 self.flush();
345 } else {
346 self.push_to_line(&t);
347 self.flush();
348 }
349 }
350 } else {
351 self.push_to_line(&t);
352 self.flush();
353 }
354 }
355 Event::FootnoteReference(_t) => {}
356 Event::SoftBreak => self.flush_word(),
357 Event::HardBreak => self.flush(),
358 Event::Rule => {
359 self.flush();
360 self.push_indent(self.indent);
361 self.push_to_line(&"_".repeat(79 - self.indent * 2));
362 self.flush();
363 }
364 Event::TaskListMarker(_b) => unimplemented!(),
365 Event::InlineHtml(..) => unimplemented!(),
366 Event::InlineMath(..) => unimplemented!(),
367 Event::DisplayMath(..) => unimplemented!(),
368 }
369 }
370 Ok(())
371 }
372
373 fn flush(&mut self) {
374 self.flush_word();
375 if !self.line.is_empty() {
376 self.output.push_str(&self.line);
377 self.output.push('\n');
378 self.line.clear();
379 }
380 }
381
382 fn hard_break(&mut self) {
383 self.flush();
384 if !self.output.ends_with("\n\n") {
385 self.output.push('\n');
386 }
387 }
388
389 fn flush_word(&mut self) {
390 if self.word.is_empty() {
391 return;
392 }
393 if self.line.len() + self.word.len() >= 79 {
394 self.output.push_str(&self.line);
395 self.output.push('\n');
396 self.line.clear();
397 }
398 if self.line.is_empty() {
399 self.push_indent(self.indent);
400 self.line.push_str(&self.word);
401 } else {
402 self.line.push(' ');
403 self.line.push_str(&self.word);
404 }
405 self.word.clear();
406 }
407
408 fn push_indent(&mut self, indent: usize) {
409 for _ in 0..indent {
410 self.line.push(' ');
411 }
412 }
413
414 fn push_to_line(&mut self, text: &str) {
415 self.flush_word();
416 self.line.push_str(text);
417 }
418}
419
420fn split_chunks(text: &str) -> Vec<&str> {
425 let mut result = Vec::new();
426 let mut start = 0;
427 while start < text.len() {
428 match text[start..].find(' ') {
429 Some(i) => {
430 if i != 0 {
431 result.push(&text[start..start + i]);
432 }
433 result.push(" ");
434 match text[start + i..].find(|c| c != ' ') {
436 Some(n) => {
437 start = start + i + n;
438 }
439 None => {
440 break;
441 }
442 }
443 }
444 None => {
445 result.push(&text[start..]);
446 break;
447 }
448 }
449 }
450 result
451}
452
453struct Table {
454 alignment: Vec<Alignment>,
455 rows: Vec<Vec<String>>,
456 row: Vec<String>,
457 cell: String,
458}
459
460impl Table {
461 fn new() -> Table {
462 Table {
463 alignment: Vec::new(),
464 rows: Vec::new(),
465 row: Vec::new(),
466 cell: String::new(),
467 }
468 }
469
470 fn process(&mut self, parser: &mut EventIter<'_>, indent: usize) -> Result<String, Error> {
472 while let Some((event, _range)) = parser.next() {
473 match event {
474 Event::Start(tag) => match tag {
475 Tag::TableHead
476 | Tag::TableRow
477 | Tag::TableCell
478 | Tag::Emphasis
479 | Tag::Strong => {}
480 Tag::Strikethrough => self.cell.push_str("~~"),
481 Tag::Link { .. } => {}
483 _ => bail!("unexpected tag in table: {:?}", tag),
484 },
485 Event::End(tag_end) => match tag_end {
486 TagEnd::Table => return self.render(indent),
487 TagEnd::TableCell => {
488 let cell = mem::replace(&mut self.cell, String::new());
489 self.row.push(cell);
490 }
491 TagEnd::TableHead | TagEnd::TableRow => {
492 let row = mem::replace(&mut self.row, Vec::new());
493 self.rows.push(row);
494 }
495 TagEnd::Strikethrough => self.cell.push_str("~~"),
496 _ => {}
497 },
498 Event::Text(t) | Event::Code(t) => {
499 self.cell.push_str(&t);
500 }
501 Event::Html(t) => bail!("html unsupported in tables: {:?}", t),
502 _ => bail!("unexpected event in table: {:?}", event),
503 }
504 }
505 bail!("table end not reached");
506 }
507
508 fn render(&self, indent: usize) -> Result<String, Error> {
509 let width_acc = vec![2; self.alignment.len()];
513 let mut col_widths = self
514 .rows
515 .iter()
516 .map(|row| row.iter().map(|cell| cell.len()))
517 .fold(width_acc, |mut acc, row| {
518 acc.iter_mut()
519 .zip(row)
520 .for_each(|(a, b)| *a = (*a).max(b + 3));
522 acc
523 });
524 let max_width = 78 - indent;
527 let total_width = col_widths.iter().sum::<usize>() + col_widths.len() + 1;
529 if total_width > max_width {
530 let to_shrink = total_width - max_width;
531 for width in &mut col_widths {
534 let percent = *width as f64 / total_width as f64;
535 *width -= (to_shrink as f64 * percent).ceil() as usize;
536 }
537 }
538 let mut result = String::new();
540
541 let mut row_line = String::new();
543 row_line.push_str(&" ".repeat(indent));
544 row_line.push('+');
545 let lines = col_widths
546 .iter()
547 .map(|width| "-".repeat(*width))
548 .collect::<Vec<_>>();
549 row_line.push_str(&lines.join("+"));
550 row_line.push('+');
551 row_line.push('\n');
552
553 result.push_str(&row_line);
555 for row in &self.rows {
557 let filled = fill_row(row, &col_widths, &self.alignment);
559 let height = filled.iter().map(|c| c.len()).max().unwrap();
562 for row_i in 0..height {
563 result.push_str(&" ".repeat(indent));
564 result.push('|');
565 for filled_row in &filled {
566 let cell = &filled_row[row_i];
567 result.push_str(cell);
568 result.push('|');
569 }
570 result.push('\n');
571 }
572 result.push_str(&row_line);
573 }
574 Ok(result)
575 }
576}
577
578fn fill_row(row: &[String], col_widths: &[usize], alignment: &[Alignment]) -> Vec<Vec<String>> {
582 let mut cell_lines = row
583 .iter()
584 .zip(col_widths)
585 .zip(alignment)
586 .map(|((cell, width), alignment)| fill_cell(cell, *width - 2, *alignment))
587 .collect::<Vec<_>>();
588 let max_lines = cell_lines.iter().map(|cell| cell.len()).max().unwrap();
590 for (cell, width) in cell_lines.iter_mut().zip(col_widths) {
591 if cell.len() < max_lines {
592 cell.extend(std::iter::repeat(" ".repeat(*width)).take(max_lines - cell.len()));
593 }
594 }
595 cell_lines
596}
597
598fn fill_cell(text: &str, width: usize, alignment: Alignment) -> Vec<String> {
602 let fill_width = |text: &str| match alignment {
603 Alignment::None | Alignment::Left => format!(" {:<width$} ", text, width = width),
604 Alignment::Center => format!(" {:^width$} ", text, width = width),
605 Alignment::Right => format!(" {:>width$} ", text, width = width),
606 };
607 if text.len() < width {
608 vec![fill_width(text)]
610 } else {
611 let mut result = Vec::new();
613 let mut line = String::new();
614 for word in text.split_whitespace() {
615 if line.len() + word.len() >= width {
616 result.push(fill_width(&line));
618 line.clear();
619 }
620 if line.is_empty() {
621 line.push_str(word);
622 } else {
623 line.push(' ');
624 line.push_str(&word);
625 }
626 }
627 if !line.is_empty() {
628 result.push(fill_width(&line));
629 }
630
631 result
632 }
633}