Print Markdown to a paper in your terminal

update pulldown cmark and support blockquote alert types

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