Print Markdown to a paper in your terminal

update pulldown cmark and support blockquote alert types

+205 -73
+10 -3
Cargo.lock
··· 836 837 [[package]] 838 name = "pulldown-cmark" 839 - version = "0.8.0" 840 source = "registry+https://github.com/rust-lang/crates.io-index" 841 - checksum = "ffade02495f22453cd593159ea2f59827aae7f53fa8323f756799b670881dcf8" 842 dependencies = [ 843 - "bitflags 1.3.2", 844 "getopts", 845 "memchr", 846 "unicase", 847 ] 848 849 [[package]] 850 name = "qoi"
··· 836 837 [[package]] 838 name = "pulldown-cmark" 839 + version = "0.12.2" 840 source = "registry+https://github.com/rust-lang/crates.io-index" 841 + checksum = "f86ba2052aebccc42cbbb3ed234b8b13ce76f75c3551a303cb2bcffcff12bb14" 842 dependencies = [ 843 + "bitflags 2.6.0", 844 "getopts", 845 "memchr", 846 + "pulldown-cmark-escape", 847 "unicase", 848 ] 849 + 850 + [[package]] 851 + name = "pulldown-cmark-escape" 852 + version = "0.11.0" 853 + source = "registry+https://github.com/rust-lang/crates.io-index" 854 + checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" 855 856 [[package]] 857 name = "qoi"
+1 -1
Cargo.toml
··· 19 [dependencies] 20 clap = { version = "4.4", features = ["derive"] } 21 terminal_size = "0.4" 22 - pulldown-cmark = "0.8" 23 ansi_term = "0.12" 24 image = "0.25" 25 console = { version = "0.15", features = ["unicode-width"] }
··· 19 [dependencies] 20 clap = { version = "4.4", features = ["derive"] } 21 terminal_size = "0.4" 22 + pulldown-cmark = "0.12" 23 ansi_term = "0.12" 24 image = "0.25" 25 console = { version = "0.15", features = ["unicode-width"] }
+3
README.md
··· 39 > Blockquotes 40 > > And even nested block quotes 41 42 9. And even images! Here's a photo of my cat 43 44 ![My cat. His name is Cato](./cato.png)
··· 39 > Blockquotes 40 > > And even nested block quotes 41 42 + > [!IMPORTANT] 43 + > Also alert blockquotes 44 + 45 9. And even images! Here's a photo of my cat 46 47 ![My cat. His name is Cato](./cato.png)
+22 -2
src/default.syncat
··· 2 background-color: brblack; 3 } 4 5 - li "prefix", 6 - blockquote "prefix" { 7 color: brblack; 8 dim: true; 9 } 10 11 rule {
··· 2 background-color: brblack; 3 } 4 5 + li & "prefix", 6 + blockquote & "prefix" { 7 color: brblack; 8 dim: true; 9 + } 10 + 11 + note-blockquote & "prefix" { 12 + color: blue; 13 + } 14 + 15 + tip-blockquote & "prefix" { 16 + color: green; 17 + } 18 + 19 + important-blockquote & "prefix" { 20 + color: purple; 21 + } 22 + 23 + warning-blockquote & "prefix" { 24 + color: yellow; 25 + } 26 + 27 + caution-blockquote & "prefix" { 28 + color: red; 29 } 30 31 rule {
+169 -67
src/printer.rs
··· 5 use ansi_term::Style; 6 use console::AnsiCodeIterator; 7 use image::{self, GenericImageView as _}; 8 - use pulldown_cmark::{Alignment, CodeBlockKind, Event, Tag}; 9 use std::convert::{TryFrom, TryInto}; 10 use std::io::{Read as _, Write as _}; 11 use std::process::{Command, Stdio}; ··· 18 Italic, 19 Bold, 20 Strikethrough, 21 - Link, 22 Caption, 23 FootnoteDefinition, 24 FootnoteReference, ··· 27 ListItem(Option<u64>, bool), 28 Code, 29 CodeBlock(String), 30 - BlockQuote, 31 Table(Vec<Alignment>), 32 TableHead, 33 TableRow, 34 TableCell, 35 - Heading(u32), 36 } 37 38 impl Scope { ··· 42 Scope::FootnoteContent => 4, 43 Scope::ListItem(..) => 4, 44 Scope::CodeBlock(..) => 2, 45 - Scope::BlockQuote => 4, 46 - Scope::Heading(2) => 5, 47 Scope::Heading(..) => 4, 48 _ => 0, 49 } ··· 51 52 fn prefix(&mut self) -> String { 53 match self { 54 - Scope::Indent => " ".to_string(), 55 - Scope::FootnoteContent => " ".to_string(), 56 Scope::ListItem(Some(index), ref mut handled) => { 57 if *handled { 58 - " ".to_string() 59 } else { 60 *handled = true; 61 format!("{: <4}", format!("{}.", index)) ··· 63 } 64 Scope::ListItem(None, ref mut handled) => { 65 if *handled { 66 - " ".to_string() 67 } else { 68 *handled = true; 69 - "• ".to_string() 70 } 71 } 72 - Scope::CodeBlock(..) => " ".to_string(), 73 - Scope::BlockQuote => "│ ".to_string(), 74 - Scope::Heading(2) => "├─── ".to_string(), 75 - Scope::Heading(..) => " ".to_string(), 76 _ => String::new(), 77 } 78 } ··· 80 fn suffix_len(&self) -> usize { 81 match self { 82 Scope::CodeBlock(..) => 2, 83 - Scope::Heading(2) => 5, 84 Scope::Heading(..) => 4, 85 _ => 0, 86 } ··· 88 89 fn suffix(&mut self) -> String { 90 match self { 91 - Scope::CodeBlock(..) => " ".to_string(), 92 - Scope::Heading(2) => " ───┤".to_string(), 93 - Scope::Heading(..) => " ".to_string(), 94 _ => String::new(), 95 } 96 } ··· 103 Italic => "emphasis", 104 Bold => "strong", 105 Strikethrough => "strikethrough", 106 - Link => "link", 107 Caption => "caption", 108 FootnoteDefinition => "footnote-def", 109 FootnoteReference => "footnote-ref", ··· 113 ListItem(..) => "li", 114 Code => "code", 115 CodeBlock(..) => "codeblock", 116 - BlockQuote => "blockquote", 117 Table(..) => "table", 118 TableHead => "th", 119 TableRow => "tr", 120 TableCell => "td", 121 - Heading(1) => "h1", 122 - Heading(2) => "h2", 123 - Heading(3) => "h3", 124 - Heading(4) => "h4", 125 - Heading(5) => "h5", 126 - Heading(6) => "h6", 127 - _ => "", 128 } 129 } 130 } ··· 345 let language_context = if lang.is_empty() || !self.opts.syncat { 346 String::from("txt") 347 } else { 348 - lang.to_string() 349 }; 350 let style = self.style3(Some(&[&language_context[..]]), None); 351 - let lang = lang.to_string(); 352 let mut first_prefix = Some(self.prefix2(Some(&[&language_context[..]]))); 353 let mut first_suffix = Some(self.suffix2(Some(&[&language_context[..]]))); 354 ··· 375 } 376 Err(error) => { 377 eprintln!("{}", error); 378 - buffer.to_string() 379 } 380 } 381 } else { ··· 587 self.empty(); 588 } 589 match tag { 590 Tag::Paragraph => { 591 self.flush(); 592 } 593 - Tag::Heading(level) => { 594 self.flush(); 595 - if level == 1 { 596 - self.print_rule(); 597 - } 598 self.scope.push(Scope::Heading(level)); 599 } 600 - Tag::BlockQuote => { 601 self.flush(); 602 - self.scope.push(Scope::BlockQuote); 603 } 604 Tag::CodeBlock(CodeBlockKind::Indented) => { 605 self.flush(); 606 - self.scope.push(Scope::CodeBlock("".to_string())); 607 } 608 Tag::CodeBlock(CodeBlockKind::Fenced(language)) => { 609 self.flush(); 610 - self.scope.push(Scope::CodeBlock(language.to_string())); 611 } 612 Tag::List(start_index) => { 613 self.flush(); 614 self.scope.push(Scope::List(start_index)); 615 } 616 Tag::Item => { 617 self.flush(); 618 if let Some(&Scope::List(index)) = self.scope.last() { ··· 659 Tag::Strikethrough => { 660 self.scope.push(Scope::Strikethrough); 661 } 662 - Tag::Link(_link_type, _destination, _title) => { 663 - self.scope.push(Scope::Link); 664 } 665 - Tag::Image(_link_type, destination, title) => { 666 self.flush(); 667 668 if !self.opts.no_images { ··· 670 .width 671 .saturating_sub(self.prefix_len()) 672 .saturating_sub(self.suffix_len()); 673 - match image::open(destination.as_ref()) { 674 Ok(image) => { 675 let (mut width, mut height) = image.dimensions(); 676 if width > available_width as u32 { ··· 704 Err(error) => { 705 self.handle_text("Cannot open image "); 706 self.scope.push(Scope::Indent); 707 - self.scope.push(Scope::Link); 708 - self.handle_text(destination); 709 self.scope.pop(); 710 self.handle_text(&format!(": {}", error)); 711 self.scope.push(Scope::Caption); ··· 721 self.handle_text(title); 722 self.scope.pop(); 723 } 724 - if !destination.is_empty() && !self.opts.hide_urls { 725 self.handle_text(" <"); 726 - self.scope.push(Scope::Link); 727 - self.handle_text(destination); 728 self.scope.pop(); 729 self.handle_text(">"); 730 } ··· 737 } 738 739 Event::End(tag) => match tag { 740 - Tag::Paragraph => { 741 self.flush(); 742 self.queue_empty(); 743 } 744 - Tag::Heading(level) => { 745 self.flush(); 746 self.scope.pop(); 747 - if level == 1 { 748 - self.print_rule(); 749 - } 750 self.queue_empty(); 751 } 752 - Tag::List(..) => { 753 self.flush(); 754 self.scope.pop(); 755 self.queue_empty(); 756 } 757 - Tag::Item => { 758 self.flush(); 759 self.scope.pop(); 760 if let Some(Scope::List(index)) = self.scope.last_mut() { 761 *index = index.map(|x| x + 1); 762 } 763 } 764 - Tag::BlockQuote => { 765 self.flush(); 766 self.scope.pop(); 767 self.queue_empty(); 768 } 769 - Tag::Table(..) => { 770 self.print_table(); 771 self.scope.pop(); 772 self.queue_empty(); 773 } 774 - Tag::CodeBlock(..) => { 775 self.flush_buffer(); 776 self.scope.pop(); 777 self.queue_empty(); 778 } 779 - Tag::Link(_link_type, destination, title) => { 780 - if !title.is_empty() && !destination.is_empty() && !self.opts.hide_urls { 781 - self.handle_text(format!(" <{}: {}>", title, destination)); 782 - } else if !destination.is_empty() && !self.opts.hide_urls { 783 - self.handle_text(format!(" <{}>", destination)); 784 } else if !title.is_empty() { 785 self.handle_text(format!(" <{}>", title)); 786 } 787 - self.scope.pop(); 788 } 789 - Tag::Image(_link_type, _destination, _title) => { 790 self.flush(); 791 self.scope.pop(); 792 self.scope.pop(); 793 self.queue_empty(); 794 } 795 - Tag::FootnoteDefinition(..) => { 796 self.flush(); 797 self.scope.pop(); 798 self.queue_empty(); ··· 813 self.handle_text(text); 814 self.scope.pop(); 815 } 816 - Event::Html(_text) => { /* unimplemented */ } 817 Event::FootnoteReference(text) => { 818 self.scope.push(Scope::FootnoteReference); 819 self.handle_text(&format!("[{}]", text));
··· 5 use ansi_term::Style; 6 use console::AnsiCodeIterator; 7 use image::{self, GenericImageView as _}; 8 + use pulldown_cmark::{Alignment, BlockQuoteKind, CodeBlockKind, Event, HeadingLevel, Tag, TagEnd}; 9 use std::convert::{TryFrom, TryInto}; 10 use std::io::{Read as _, Write as _}; 11 use std::process::{Command, Stdio}; ··· 18 Italic, 19 Bold, 20 Strikethrough, 21 + Link { dest_url: String, title: String }, 22 Caption, 23 FootnoteDefinition, 24 FootnoteReference, ··· 27 ListItem(Option<u64>, bool), 28 Code, 29 CodeBlock(String), 30 + BlockQuote(Option<BlockQuoteKind>), 31 Table(Vec<Alignment>), 32 TableHead, 33 TableRow, 34 TableCell, 35 + Heading(HeadingLevel), 36 } 37 38 impl Scope { ··· 42 Scope::FootnoteContent => 4, 43 Scope::ListItem(..) => 4, 44 Scope::CodeBlock(..) => 2, 45 + Scope::BlockQuote(..) => 4, 46 + Scope::Heading(HeadingLevel::H2) => 5, 47 Scope::Heading(..) => 4, 48 _ => 0, 49 } ··· 51 52 fn prefix(&mut self) -> String { 53 match self { 54 + Scope::Indent => " ".to_owned(), 55 + Scope::FootnoteContent => " ".to_owned(), 56 Scope::ListItem(Some(index), ref mut handled) => { 57 if *handled { 58 + " ".to_owned() 59 } else { 60 *handled = true; 61 format!("{: <4}", format!("{}.", index)) ··· 63 } 64 Scope::ListItem(None, ref mut handled) => { 65 if *handled { 66 + " ".to_owned() 67 } else { 68 *handled = true; 69 + "• ".to_owned() 70 } 71 } 72 + Scope::CodeBlock(..) => " ".to_owned(), 73 + Scope::BlockQuote(..) => "┃ ".to_owned(), 74 + Scope::Heading(HeadingLevel::H2) => "├─── ".to_owned(), 75 + Scope::Heading(..) => " ".to_owned(), 76 _ => String::new(), 77 } 78 } ··· 80 fn suffix_len(&self) -> usize { 81 match self { 82 Scope::CodeBlock(..) => 2, 83 + Scope::Heading(HeadingLevel::H2) => 5, 84 Scope::Heading(..) => 4, 85 _ => 0, 86 } ··· 88 89 fn suffix(&mut self) -> String { 90 match self { 91 + Scope::CodeBlock(..) => " ".to_owned(), 92 + Scope::Heading(HeadingLevel::H2) => " ───┤".to_owned(), 93 + Scope::Heading(..) => " ".to_owned(), 94 _ => String::new(), 95 } 96 } ··· 103 Italic => "emphasis", 104 Bold => "strong", 105 Strikethrough => "strikethrough", 106 + Link { .. } => "link", 107 Caption => "caption", 108 FootnoteDefinition => "footnote-def", 109 FootnoteReference => "footnote-ref", ··· 113 ListItem(..) => "li", 114 Code => "code", 115 CodeBlock(..) => "codeblock", 116 + BlockQuote(None) => "blockquote", 117 + BlockQuote(Some(BlockQuoteKind::Note)) => "note-blockquote", 118 + BlockQuote(Some(BlockQuoteKind::Tip)) => "tip-blockquote", 119 + BlockQuote(Some(BlockQuoteKind::Important)) => "important-blockquote", 120 + BlockQuote(Some(BlockQuoteKind::Warning)) => "warning-blockquote", 121 + BlockQuote(Some(BlockQuoteKind::Caution)) => "caution-blockquote", 122 Table(..) => "table", 123 TableHead => "th", 124 TableRow => "tr", 125 TableCell => "td", 126 + Heading(HeadingLevel::H1) => "h1", 127 + Heading(HeadingLevel::H2) => "h2", 128 + Heading(HeadingLevel::H3) => "h3", 129 + Heading(HeadingLevel::H4) => "h4", 130 + Heading(HeadingLevel::H5) => "h5", 131 + Heading(HeadingLevel::H6) => "h6", 132 } 133 } 134 } ··· 349 let language_context = if lang.is_empty() || !self.opts.syncat { 350 String::from("txt") 351 } else { 352 + lang.to_owned() 353 }; 354 let style = self.style3(Some(&[&language_context[..]]), None); 355 + let lang = lang.to_owned(); 356 let mut first_prefix = Some(self.prefix2(Some(&[&language_context[..]]))); 357 let mut first_suffix = Some(self.suffix2(Some(&[&language_context[..]]))); 358 ··· 379 } 380 Err(error) => { 381 eprintln!("{}", error); 382 + buffer.to_owned() 383 } 384 } 385 } else { ··· 591 self.empty(); 592 } 593 match tag { 594 + Tag::MetadataBlock(..) => self.scope.push(Scope::CodeBlock("".to_owned())), 595 + Tag::HtmlBlock => {} 596 Tag::Paragraph => { 597 self.flush(); 598 } 599 + Tag::Heading { 600 + level: HeadingLevel::H1, 601 + .. 602 + } => { 603 self.flush(); 604 + self.print_rule(); 605 + self.scope.push(Scope::Heading(HeadingLevel::H1)); 606 + } 607 + Tag::Heading { level, .. } => { 608 + self.flush(); 609 self.scope.push(Scope::Heading(level)); 610 } 611 + Tag::BlockQuote(kind) => { 612 self.flush(); 613 + self.scope.push(Scope::BlockQuote(kind)); 614 + match kind { 615 + None => {} 616 + Some(BlockQuoteKind::Note) => { 617 + let style = Self::resolve_scopes( 618 + &self.stylesheet, 619 + &["note-blockquote"], 620 + Some("prefix"), 621 + ); 622 + self.handle_text(&format!( 623 + "{} {}", 624 + style.paint("󰋽"), 625 + style.paint("Note") 626 + )); 627 + } 628 + Some(BlockQuoteKind::Tip) => { 629 + let style = Self::resolve_scopes( 630 + &self.stylesheet, 631 + &["tip-blockquote"], 632 + Some("prefix"), 633 + ); 634 + self.handle_text(&format!( 635 + "{} {}", 636 + style.paint("󰌶"), 637 + style.paint("Tip") 638 + )); 639 + } 640 + Some(BlockQuoteKind::Important) => { 641 + let style = Self::resolve_scopes( 642 + &self.stylesheet, 643 + &["important-blockquote"], 644 + Some("prefix"), 645 + ); 646 + self.handle_text(&format!( 647 + "{} {}", 648 + style.paint("󱋉"), 649 + style.paint("Important") 650 + )); 651 + } 652 + Some(BlockQuoteKind::Warning) => { 653 + let style = Self::resolve_scopes( 654 + &self.stylesheet, 655 + &["warning-blockquote"], 656 + Some("prefix"), 657 + ); 658 + self.handle_text(&format!( 659 + "{} {}", 660 + style.paint("󰀪"), 661 + style.paint("Warning") 662 + )); 663 + } 664 + Some(BlockQuoteKind::Caution) => { 665 + let style = Self::resolve_scopes( 666 + &self.stylesheet, 667 + &["caution-blockquote"], 668 + Some("prefix"), 669 + ); 670 + self.handle_text(&format!( 671 + "{} {}", 672 + style.paint("󰳦"), 673 + style.paint("Caution") 674 + )); 675 + } 676 + } 677 } 678 Tag::CodeBlock(CodeBlockKind::Indented) => { 679 self.flush(); 680 + self.scope.push(Scope::CodeBlock("".to_owned())); 681 } 682 Tag::CodeBlock(CodeBlockKind::Fenced(language)) => { 683 self.flush(); 684 + self.scope.push(Scope::CodeBlock(language.into_string())); 685 } 686 Tag::List(start_index) => { 687 self.flush(); 688 self.scope.push(Scope::List(start_index)); 689 } 690 + Tag::DefinitionList => {} 691 + Tag::DefinitionListTitle => {} 692 + Tag::DefinitionListDefinition => {} 693 Tag::Item => { 694 self.flush(); 695 if let Some(&Scope::List(index)) = self.scope.last() { ··· 736 Tag::Strikethrough => { 737 self.scope.push(Scope::Strikethrough); 738 } 739 + Tag::Link { 740 + dest_url, title, .. 741 + } => { 742 + self.scope.push(Scope::Link { 743 + dest_url: dest_url.into_string(), 744 + title: title.into_string(), 745 + }); 746 } 747 + Tag::Image { 748 + dest_url, title, .. 749 + } => { 750 self.flush(); 751 752 if !self.opts.no_images { ··· 754 .width 755 .saturating_sub(self.prefix_len()) 756 .saturating_sub(self.suffix_len()); 757 + match image::open(dest_url.as_ref()) { 758 Ok(image) => { 759 let (mut width, mut height) = image.dimensions(); 760 if width > available_width as u32 { ··· 788 Err(error) => { 789 self.handle_text("Cannot open image "); 790 self.scope.push(Scope::Indent); 791 + self.scope.push(Scope::Link { 792 + dest_url: "".to_owned(), 793 + title: "".to_owned(), 794 + }); 795 + self.handle_text(dest_url); 796 self.scope.pop(); 797 self.handle_text(&format!(": {}", error)); 798 self.scope.push(Scope::Caption); ··· 808 self.handle_text(title); 809 self.scope.pop(); 810 } 811 + if !dest_url.is_empty() && !self.opts.hide_urls { 812 self.handle_text(" <"); 813 + self.scope.push(Scope::Link { 814 + dest_url: "".to_owned(), 815 + title: "".to_owned(), 816 + }); 817 + self.handle_text(dest_url); 818 self.scope.pop(); 819 self.handle_text(">"); 820 } ··· 827 } 828 829 Event::End(tag) => match tag { 830 + TagEnd::Paragraph => { 831 self.flush(); 832 self.queue_empty(); 833 } 834 + TagEnd::Heading(HeadingLevel::H1) => { 835 self.flush(); 836 self.scope.pop(); 837 + self.print_rule(); 838 self.queue_empty(); 839 } 840 + TagEnd::Heading(_) => { 841 + self.flush(); 842 + self.scope.pop(); 843 + self.queue_empty(); 844 + } 845 + TagEnd::List(..) => { 846 self.flush(); 847 self.scope.pop(); 848 self.queue_empty(); 849 } 850 + TagEnd::Item => { 851 self.flush(); 852 self.scope.pop(); 853 if let Some(Scope::List(index)) = self.scope.last_mut() { 854 *index = index.map(|x| x + 1); 855 } 856 } 857 + TagEnd::BlockQuote(..) => { 858 self.flush(); 859 self.scope.pop(); 860 self.queue_empty(); 861 } 862 + TagEnd::Table => { 863 self.print_table(); 864 self.scope.pop(); 865 self.queue_empty(); 866 } 867 + TagEnd::HtmlBlock => {} 868 + TagEnd::CodeBlock => { 869 self.flush_buffer(); 870 self.scope.pop(); 871 self.queue_empty(); 872 } 873 + TagEnd::Link => { 874 + let Scope::Link { dest_url, title } = self.scope.pop().unwrap() else { 875 + panic!() 876 + }; 877 + if !title.is_empty() && !dest_url.is_empty() && !self.opts.hide_urls { 878 + self.handle_text(format!(" <{}: {}>", title, dest_url)); 879 + } else if !dest_url.is_empty() && !self.opts.hide_urls { 880 + self.handle_text(format!(" <{}>", dest_url)); 881 } else if !title.is_empty() { 882 self.handle_text(format!(" <{}>", title)); 883 } 884 } 885 + TagEnd::Image => { 886 self.flush(); 887 self.scope.pop(); 888 self.scope.pop(); 889 self.queue_empty(); 890 } 891 + TagEnd::FootnoteDefinition => { 892 self.flush(); 893 self.scope.pop(); 894 self.queue_empty(); ··· 909 self.handle_text(text); 910 self.scope.pop(); 911 } 912 + Event::Html(_text) => { /* not rendered */ } 913 + Event::InlineHtml(_text) => { /* not rendered */ } 914 + Event::InlineMath(text) | Event::DisplayMath(text) => { 915 + self.scope.push(Scope::Code); 916 + self.handle_text(text); 917 + self.scope.pop(); 918 + } 919 Event::FootnoteReference(text) => { 920 self.scope.push(Scope::FootnoteReference); 921 self.handle_text(&format!("[{}]", text));