├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── Cargo.toml ├── README.md ├── pulldown_mdbook ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── README.md └── src │ ├── lib.rs │ ├── markdown.rs │ └── parser.rs ├── pulldown_typst ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── README.md └── src │ ├── lib.rs │ └── markup.rs └── pullup ├── CHANGELOG.md ├── Cargo.toml ├── README.md └── src ├── assert.rs ├── filter.rs ├── lib.rs ├── markdown ├── mod.rs ├── strip.rs └── to │ ├── mod.rs │ └── typst.rs ├── mdbook ├── mod.rs └── to │ ├── mod.rs │ └── typst │ ├── builder.rs │ └── mod.rs └── typst ├── mod.rs └── to ├── markup.rs └── mod.rs /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | schedule: [cron: "40 1 * * *"] 8 | 9 | permissions: 10 | contents: read 11 | 12 | env: 13 | RUSTFLAGS: -Dwarnings 14 | 15 | jobs: 16 | stable: 17 | name: Rust ${{matrix.rust}} 18 | runs-on: ubuntu-latest 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | rust: [stable, beta] 23 | timeout-minutes: 45 24 | steps: 25 | - uses: actions/checkout@v4 26 | - uses: dtolnay/rust-toolchain@master 27 | with: 28 | toolchain: ${{matrix.rust}} 29 | - run: cd pullup && cargo build 30 | - run: cd pullup && cargo build --no-default-features 31 | - run: cd pullup && cargo build --all-features 32 | - run: cd pullup && cargo build --no-default-features --features builder 33 | - run: cd pullup && cargo build --no-default-features --features markdown 34 | - run: cd pullup && cargo build --no-default-features --features mdbook 35 | - run: cd pullup && cargo build --no-default-features --features typst 36 | - run: cd pullup && cargo test --features markdown,mdbook 37 | - run: cd pullup && cargo test --features markdown,typst 38 | - run: cd pullup && cargo test --features mdbook,typst 39 | - run: cd pulldown_mdbook && cargo build 40 | - run: cd pulldown_mdbook && cargo test 41 | - run: cd pulldown_typst && cargo build 42 | - run: cd pulldown_typst && cargo test 43 | 44 | nightly: 45 | name: Rust nightly ${{matrix.os == 'windows' && '(windows)' || ''}} 46 | runs-on: ${{matrix.os}}-latest 47 | strategy: 48 | fail-fast: false 49 | matrix: 50 | os: [ubuntu, windows] 51 | timeout-minutes: 45 52 | steps: 53 | - uses: actions/checkout@v4 54 | - uses: dtolnay/rust-toolchain@nightly 55 | - run: cd pullup && cargo build 56 | - run: cd pullup && cargo build --no-default-features 57 | - run: cd pullup && cargo build --all-features 58 | - run: cd pullup && cargo build --no-default-features --features builder 59 | - run: cd pullup && cargo build --no-default-features --features markdown 60 | - run: cd pullup && cargo build --no-default-features --features mdbook 61 | - run: cd pullup && cargo build --no-default-features --features typst 62 | - run: cd pullup && cargo test --features markdown,mdbook 63 | - run: cd pullup && cargo test --features markdown,typst 64 | - run: cd pullup && cargo test --features mdbook,typst 65 | - run: cd pulldown_mdbook && cargo build 66 | - run: cd pulldown_mdbook && cargo test 67 | - run: cd pulldown_typst && cargo build 68 | - run: cd pulldown_typst && cargo test 69 | 70 | doc: 71 | name: Documentation 72 | runs-on: ubuntu-latest 73 | timeout-minutes: 45 74 | env: 75 | RUSTDOCFLAGS: -Dwarnings 76 | steps: 77 | - uses: actions/checkout@v4 78 | - uses: dtolnay/rust-toolchain@nightly 79 | - uses: dtolnay/install@cargo-docs-rs 80 | - run: cargo docs-rs -p pullup 81 | - run: cargo docs-rs -p pulldown_mdbook 82 | - run: cargo docs-rs -p pulldown_typst 83 | 84 | clippy: 85 | name: Clippy 86 | runs-on: ubuntu-latest 87 | if: github.event_name != 'pull_request' 88 | timeout-minutes: 45 89 | steps: 90 | - uses: actions/checkout@v4 91 | - uses: dtolnay/rust-toolchain@clippy 92 | - run: cd pullup && cargo clippy -- -Dclippy::all 93 | - run: cd pullup && cargo clippy --no-default-features -- -Dclippy::all 94 | - run: cd pullup && cargo clippy --all-features -- -Dclippy::all 95 | - run: cd pulldown_mdbook && cargo clippy -- -Dclippy::all 96 | - run: cd pulldown_typst && cargo clippy -- -Dclippy::all 97 | 98 | outdated: 99 | name: Outdated 100 | runs-on: ubuntu-latest 101 | if: github.event_name != 'pull_request' 102 | timeout-minutes: 45 103 | steps: 104 | - uses: actions/checkout@v4 105 | - uses: dtolnay/install@cargo-outdated 106 | - run: cargo outdated --workspace --exit-code 1 107 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .vscode/ 3 | **/*.rs.bk 4 | *.sw[po] 5 | Cargo.lock 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["pullup", "pulldown_mdbook", "pulldown_typst"] 3 | resolver = "2" 4 | 5 | [patch.crates-io] 6 | pullup = { path = "pullup" } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pullup 2 | 3 | **Pullup** converts between [*pull*down 4 | parser](https://github.com/raphlinus/pulldown-cmark#why-a-pull-parser) events for 5 | various mark*up* formats. 6 | 7 | Currently supported markup formats: 8 | 9 | - [Markdown](https://commonmark.org/) (via the `markdown` feature) 10 | - [mdBook](https://github.com/rust-lang/mdBook) (via the `mdbook` feature) 11 | - [Typst](https://github.com/typst/typst) (via the `typst` feature) 12 | 13 | Formats are disabled by default and must be enabled via features before use. 14 | -------------------------------------------------------------------------------- /pulldown_mdbook/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /pulldown_mdbook/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## v0.3.2 (2024-09-08) 9 | 10 | ### Other 11 | 12 | - Add support for markdown tables. 13 | 14 | ### Commit Statistics 15 | 16 | 17 | 18 | - 1 commit contributed to the release. 19 | - 281 days passed between releases. 20 | - 1 commit was understood as [conventional](https://www.conventionalcommits.org). 21 | - 0 issues like '(#ID)' were seen in commit messages 22 | 23 | ### Commit Details 24 | 25 | 26 | 27 |
view details 28 | 29 | * **Uncategorized** 30 | - Add support for markdown tables. ([`27e2536`](https://github.com/LegNeato/pullup/commit/27e25364a5c1f15ce386e559cf3d9cea9fed4aa1)) 31 |
32 | 33 | ## v0.3.1 (2023-12-01) 34 | 35 | ### Bug Fixes 36 | 37 | - try to fix cargo-smart-release. 38 | 39 | ### Commit Statistics 40 | 41 | 42 | 43 | - 13 commits contributed to the release over the course of 9 calendar days. 44 | - 1 commit was understood as [conventional](https://www.conventionalcommits.org). 45 | - 0 issues like '(#ID)' were seen in commit messages 46 | 47 | ### Commit Details 48 | 49 | 50 | 51 |
view details 52 | 53 | * **Uncategorized** 54 | - Release pulldown_mdbook v0.3.1, pulldown_typst v0.3.1, pullup v0.3.1 ([`e565ece`](https://github.com/LegNeato/pullup/commit/e565ece82bcc04226211f278f0bbbefe7754ff68)) 55 | - Release pulldown_mdbook v0.3.0, pulldown_typst v0.3.0, pullup v0.3.0 ([`2c88246`](https://github.com/LegNeato/pullup/commit/2c88246b29b36560060646dcbccedeb791097c36)) 56 | - Release pulldown_mdbook v0.3.0, pulldown_typst v0.3.0, pullup v0.3.0 ([`8e2d360`](https://github.com/LegNeato/pullup/commit/8e2d36063d64727a04101e29a1c7b7cd231f31f2)) 57 | - Try to fix cargo-smart-release. ([`6f1e1b4`](https://github.com/LegNeato/pullup/commit/6f1e1b495e53fdf1936ccf25f6f3e26ae26e3d20)) 58 | - Adjusting changelogs prior to release of pulldown_mdbook v0.3.0, pulldown_typst v0.3.0, pullup v0.3.0 ([`b0018bf`](https://github.com/LegNeato/pullup/commit/b0018bf5064900690b490ddf8c3647356bce40c7)) 59 | - Add changelogs. ([`e89557b`](https://github.com/LegNeato/pullup/commit/e89557be19304844054a26622c6b1e28987f0937)) 60 | - Add tracing, change some converters. ([`8b2e292`](https://github.com/LegNeato/pullup/commit/8b2e2921fc3a5cf1a3d2ce7a46ddd3867f75479a)) 61 | - Add markers to distinguish mdbook configuration from content. ([`2c724c9`](https://github.com/LegNeato/pullup/commit/2c724c9f35876a7a80195a0711ecd7160b6b997d)) 62 | - Bump versions. ([`3ceaa03`](https://github.com/LegNeato/pullup/commit/3ceaa03661aae8f890d62e3ac90fd4c1e8e55b56)) 63 | - Clippy. ([`a10d5eb`](https://github.com/LegNeato/pullup/commit/a10d5eb9e448099ec1d5c5c74435b59a2b454b57)) 64 | - Add mdbook parser and make docs consistent. ([`91b4f88`](https://github.com/LegNeato/pullup/commit/91b4f88596430ffd2560a216e40080f89a38697c)) 65 | - Improve docs for mdbook. ([`e7c099a`](https://github.com/LegNeato/pullup/commit/e7c099a2fae0befbad7b9e3c84eb67814c287015)) 66 | - Bring in other crates. ([`1ab5157`](https://github.com/LegNeato/pullup/commit/1ab51574957a2a7c1643145f13c0e13322755861)) 67 |
68 | 69 | ## v0.3.0 (2023-12-01) 70 | 71 | ### Bug Fixes 72 | 73 | - try to fix cargo-smart-release. 74 | 75 | -------------------------------------------------------------------------------- /pulldown_mdbook/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pulldown_mdbook" 3 | version = "0.3.2" 4 | description = "A pull parser for mdBook" 5 | license = "MIT OR Apache-2.0" 6 | edition = "2021" 7 | authors = ["Christian Legnitto "] 8 | 9 | [features] 10 | tracing = ["dep:tracing"] 11 | 12 | [dependencies] 13 | mdbook = { version = "0.4.35", default-features = false } 14 | pulldown-cmark = "0.9.3" 15 | tracing = { version = "0.1.40", optional = true } 16 | 17 | [dev-dependencies] 18 | similar-asserts = "1.5.0" 19 | -------------------------------------------------------------------------------- /pulldown_mdbook/README.md: -------------------------------------------------------------------------------- 1 | # pulldown_mdbook 2 | 3 | This library is a pull parser for books created with 4 | [mdBook](https://github.com/rust-lang/mdBook). 5 | 6 | It's in a rough yet working state. -------------------------------------------------------------------------------- /pulldown_mdbook/src/lib.rs: -------------------------------------------------------------------------------- 1 | use pulldown_cmark::CowStr; 2 | use std::path::PathBuf; 3 | 4 | pub mod markdown; 5 | pub mod parser; 6 | 7 | pub use parser::Parser; 8 | 9 | use markdown::TextMergeStream; 10 | 11 | #[derive(Debug, Clone, PartialEq)] 12 | pub enum Event<'a> { 13 | /// Start of a tagged element. Events that are yielded after this event 14 | /// and before its corresponding `End` event are inside this element. 15 | /// Start and end events are guaranteed to be balanced. 16 | Start(Tag<'a>), 17 | /// End of a tagged element. 18 | End(Tag<'a>), 19 | /// The root path of the book. 20 | Root(PathBuf), 21 | /// The title of the book. 22 | Title(CowStr<'a>), 23 | /// An author of the book. 24 | Author(CowStr<'a>), 25 | /// Separators can be added before, in-between, and after any other element. 26 | Separator, 27 | /// Parsed markdown content. 28 | MarkdownContentEvent(pulldown_cmark::Event<'a>), 29 | } 30 | 31 | /// Tags for elements that can contain other elements. 32 | #[derive(Clone, Debug, PartialEq)] 33 | pub enum Tag<'a> { 34 | /// A part is used to logically separate different sections of the book. The first 35 | /// field is the title. If the part is ordered the second field indicates the number 36 | /// of the first chapter. 37 | Part(Option>, Option), 38 | 39 | /// A chapter represents book content. The first field indicates the status, the 40 | /// second field is the name, and the third field is the source. If the part is 41 | /// ordered the fourth field indicates the number of the chapter. Chapters can be 42 | /// nested. 43 | Chapter( 44 | ChapterStatus, 45 | CowStr<'a>, 46 | Option>, 47 | Option, 48 | ), 49 | /// The content of the chapter. 50 | Content(ContentType), 51 | 52 | /// A list of the mdbook authors. Only contains Author events. 53 | AuthorList, 54 | 55 | /// Logical marker for the configuration of the book. 56 | BookConfiguration, 57 | 58 | /// Logical marker for the content of the book. 59 | BookContent, 60 | } 61 | 62 | /// The status of a chapter. 63 | #[derive(Clone, Debug, PartialEq, Copy)] 64 | pub enum ChapterStatus { 65 | Active, 66 | Draft, 67 | } 68 | 69 | /// The type of content. 70 | #[derive(Clone, Debug, PartialEq, Copy)] 71 | pub enum ContentType { 72 | Markdown, 73 | } 74 | 75 | /// The source of a chapter. 76 | #[derive(Clone, Debug, PartialEq)] 77 | pub enum ChapterSource<'a> { 78 | Url(CowStr<'a>), 79 | Path(PathBuf), 80 | } 81 | -------------------------------------------------------------------------------- /pulldown_mdbook/src/markdown.rs: -------------------------------------------------------------------------------- 1 | pub use pulldown_cmark::{Event, HeadingLevel, LinkType, Parser, Tag}; 2 | 3 | use crate::CowStr; 4 | 5 | // Copied from `pulldown-cmark` until we upgrade our version. 6 | #[derive(Debug)] 7 | pub struct TextMergeStream<'a, I> { 8 | iter: I, 9 | last_event: Option>, 10 | } 11 | 12 | impl<'a, I> TextMergeStream<'a, I> 13 | where 14 | I: Iterator>, 15 | { 16 | pub fn new(iter: I) -> Self { 17 | Self { 18 | iter, 19 | last_event: None, 20 | } 21 | } 22 | } 23 | 24 | impl<'a, I> Iterator for TextMergeStream<'a, I> 25 | where 26 | I: Iterator>, 27 | { 28 | type Item = Event<'a>; 29 | 30 | fn next(&mut self) -> Option { 31 | match (self.last_event.take(), self.iter.next()) { 32 | (Some(Event::Text(last_text)), Some(Event::Text(next_text))) => { 33 | // We need to start merging consecutive text events together into one 34 | let mut string_buf: String = last_text.into_string(); 35 | string_buf.push_str(&next_text); 36 | loop { 37 | // Avoid recursion to avoid stack overflow and to optimize concatenation 38 | match self.iter.next() { 39 | Some(Event::Text(next_text)) => { 40 | string_buf.push_str(&next_text); 41 | } 42 | next_event => { 43 | self.last_event = next_event; 44 | if string_buf.is_empty() { 45 | // Discard text event(s) altogether if there is no text 46 | break self.next(); 47 | } else { 48 | break Some(Event::Text(CowStr::Boxed( 49 | string_buf.into_boxed_str(), 50 | ))); 51 | } 52 | } 53 | } 54 | } 55 | } 56 | (None, Some(next_event)) => { 57 | // This only happens once during the first iteration and if there are items 58 | self.last_event = Some(next_event); 59 | self.next() 60 | } 61 | (None, None) => { 62 | // This happens when the iterator is depleted 63 | None 64 | } 65 | (last_event, next_event) => { 66 | // The ordinary case, emit one event after the other without modification 67 | self.last_event = next_event; 68 | last_event 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /pulldown_mdbook/src/parser.rs: -------------------------------------------------------------------------------- 1 | //! Parsers to Convert mdBook into an [`Event`] iterator. 2 | use crate::*; 3 | use core::iter; 4 | use mdbook::{renderer::RenderContext, BookItem, Config, MDBook}; 5 | 6 | #[derive(Default, Debug)] 7 | enum ConfigState { 8 | #[default] 9 | Start, 10 | Title, 11 | AuthorList, 12 | Author(usize), 13 | Done, 14 | } 15 | 16 | /// Parse an mdBook configuration into events. 17 | #[derive(Debug)] 18 | pub struct ConfigParser<'a> { 19 | state: ConfigState, 20 | config: &'a Config, 21 | } 22 | 23 | impl<'a> ConfigParser<'a> { 24 | pub fn new(config: &'a Config) -> Self { 25 | Self { 26 | config, 27 | state: ConfigState::default(), 28 | } 29 | } 30 | } 31 | impl<'a> Iterator for ConfigParser<'a> { 32 | type Item = self::Event<'a>; 33 | 34 | fn next(&mut self) -> Option { 35 | match self.state { 36 | ConfigState::Start => { 37 | self.state = ConfigState::Title; 38 | if let Some(title) = self.config.book.title.as_ref() { 39 | Some(self::Event::Title(title.clone().into())) 40 | } else { 41 | self.next() 42 | } 43 | } 44 | ConfigState::Title => { 45 | if !self.config.book.authors.is_empty() { 46 | self.state = ConfigState::AuthorList; 47 | Some(Event::Start(Tag::AuthorList)) 48 | } else { 49 | self.state = ConfigState::Done; 50 | self.next() 51 | } 52 | } 53 | ConfigState::AuthorList => { 54 | self.state = ConfigState::Author(1); 55 | let author = self 56 | .config 57 | .book 58 | .authors 59 | .first() 60 | .expect("author in author list"); 61 | Some(Event::Author(author.clone().into())) 62 | } 63 | ConfigState::Author(index) => { 64 | if index >= self.config.book.authors.len() { 65 | self.state = ConfigState::Done; 66 | Some(Event::End(Tag::AuthorList)) 67 | } else { 68 | self.state = ConfigState::Author(index + 1); 69 | let author = self 70 | .config 71 | .book 72 | .authors 73 | .get(index) 74 | .expect("author index under length"); 75 | Some(Event::Author(author.clone().into())) 76 | } 77 | } 78 | ConfigState::Done => None, 79 | } 80 | } 81 | } 82 | 83 | // This is super janky. I have a branch breaking this into iterators but am stuck with 84 | // lifetimes not living long enough for the embedded markdown events. 85 | fn events_from_items<'a, 'b>(items: &'a [BookItem]) -> Vec> 86 | where 87 | 'a: 'b, 88 | { 89 | let (_, events) = items 90 | .iter() 91 | .fold((None, vec![]), |mut acc, item| match item { 92 | BookItem::Chapter(ch) => { 93 | let status = if ch.is_draft_chapter() { 94 | ChapterStatus::Draft 95 | } else { 96 | ChapterStatus::Active 97 | }; 98 | let name = ch.name.clone(); 99 | let source = ch 100 | .source_path 101 | .as_ref() 102 | .map(|x| ChapterSource::Path(x.to_owned())); 103 | 104 | // Chapter start event. 105 | acc.1.push(self::Event::Start(self::Tag::Chapter( 106 | status, 107 | name.clone().into(), 108 | source.clone(), 109 | None, 110 | ))); 111 | 112 | // Chapter content events. 113 | if !ch.content.is_empty() { 114 | let p = TextMergeStream::new(pulldown_cmark::Parser::new_ext( 115 | &ch.content, 116 | pulldown_cmark::Options::ENABLE_TABLES, 117 | )); 118 | let p = p.map(Event::MarkdownContentEvent); 119 | acc.1.extend(p); 120 | acc.1.push(Event::End(Tag::Content(ContentType::Markdown))); 121 | }; 122 | 123 | if !ch.sub_items.is_empty() { 124 | let subevents = events_from_items(&ch.sub_items); 125 | acc.1.extend(subevents); 126 | } 127 | 128 | // Chapter end event. 129 | acc.1 130 | .push(Event::End(Tag::Chapter(status, name.into(), source, None))); 131 | acc 132 | } 133 | BookItem::Separator => { 134 | acc.1.push(self::Event::Separator); 135 | acc 136 | } 137 | // TODO: numbering. 138 | BookItem::PartTitle(x) => { 139 | let ev = if let Some(current_title) = acc.0 { 140 | // Close the current part and start a new one. 141 | vec![ 142 | self::Event::End(self::Tag::Part(Some(current_title), None)), 143 | self::Event::Start(self::Tag::Part(Some(x.clone().into()), None)), 144 | ] 145 | } else { 146 | // Start a new part with a title. 147 | vec![self::Event::Start(self::Tag::Part( 148 | Some(x.clone().into()), 149 | None, 150 | ))] 151 | }; 152 | acc.1.extend(ev); 153 | (Some(x.clone().into()), acc.1) 154 | } 155 | }); 156 | 157 | // Post-process to insert `Part` events. The mdbook data model kinda has these, 158 | // kinda does not. We'll be consistent and make all chapters contained in parts. 159 | let first_start_pos = events 160 | .iter() 161 | .position(|x| matches!(x, self::Event::Start(self::Tag::Part(_, _)))); 162 | let first_end_pos = events 163 | .iter() 164 | .position(|x| matches!(x, self::Event::End(self::Tag::Part(_, _)))); 165 | let last_start_pos = events 166 | .iter() 167 | .rposition(|x| matches!(x, self::Event::Start(self::Tag::Part(_, _)))); 168 | let last_end_pos = events 169 | .iter() 170 | .rposition(|x| matches!(x, self::Event::End(self::Tag::Part(_, _)))); 171 | 172 | match (first_start_pos, first_end_pos, last_start_pos, last_end_pos) { 173 | // No parts / titles at all, wrap the whole thing in an untitled part. 174 | (None, None, _, _) => iter::once(self::Event::Start(self::Tag::Part(None, None))) 175 | .chain(events) 176 | .chain(iter::once(self::Event::End(self::Tag::Part(None, None)))) 177 | .collect(), 178 | // Only ends, missing starts. 179 | (None, Some(p), None, Some(_)) => { 180 | // Add a start for the first end. 181 | let start = match &events[p] { 182 | Event::End(tag) => Event::Start(tag.clone()), 183 | _ => unreachable!(), 184 | }; 185 | iter::once(start).chain(events.iter().cloned()).collect() 186 | } 187 | // Only starts, missing ends. 188 | (Some(first_start), None, Some(last_start), None) => { 189 | // Add an end for the last start. 190 | let end = match &events[last_start] { 191 | Event::Start(tag) => Event::End(tag.clone()), 192 | _ => unreachable!(), 193 | }; 194 | if first_start != 0 { 195 | // Synthesize starting part, and end the first part where the second 196 | // part starts. 197 | let (inside, outside) = events.split_at(first_start); 198 | return iter::once(Event::Start(Tag::Part(None, None))) 199 | .chain(inside.iter().cloned()) 200 | .chain(iter::once(Event::End(Tag::Part(None, None)))) 201 | .chain(outside.iter().cloned()) 202 | // Put our new end on. 203 | .chain(iter::once(end)) 204 | .collect(); 205 | } else { 206 | // Just the end. 207 | events.iter().cloned().chain(iter::once(end)).collect() 208 | } 209 | } 210 | 211 | // Contains both starts and ends, we need to make sure they are matched. 212 | (Some(first_start), Some(first_end), Some(last_start), Some(last_end)) => { 213 | // End before start, so we need to create a start for the first end. 214 | if first_end < first_start { 215 | let start = match &events[first_end] { 216 | Event::End(tag) => Event::Start(tag.clone()), 217 | _ => unreachable!(), 218 | }; 219 | return iter::once(start).chain(events.iter().cloned()).collect(); 220 | } 221 | 222 | // Start after end, so we need to create an end for the last start. 223 | if last_start > last_end { 224 | let end = match &events[last_start] { 225 | Event::Start(tag) => Event::End(tag.clone()), 226 | _ => unreachable!(), 227 | }; 228 | return events.iter().cloned().chain(iter::once(end)).collect(); 229 | } 230 | 231 | // Everything is matched, just return what is there. 232 | events 233 | } 234 | // If we find a part, it will be found forwards and backwards. Therefore, the 235 | // other combinations are not possible. 236 | _ => unreachable!(), 237 | } 238 | } 239 | 240 | /// Parse an mdBook structure into events. 241 | // TODO: tests 242 | #[derive(Debug, Clone)] 243 | pub struct Parser<'a>(Vec>); 244 | 245 | impl<'a> Parser<'a> { 246 | /// Create a parser from an `MDBook`. This is available when using `mdbook` as a 247 | /// library. 248 | pub fn from_mdbook(book: &'a MDBook) -> Self { 249 | let config = ConfigParser::new(&book.config); 250 | let events = iter::empty() 251 | // Configuration 252 | .chain(iter::once(self::Event::Start(self::Tag::BookConfiguration))) 253 | .chain(iter::once(self::Event::Root(book.root.to_owned().clone()))) 254 | .chain(config) 255 | .chain(iter::once(self::Event::End(self::Tag::BookConfiguration))) 256 | // Content 257 | .chain(iter::once(self::Event::Start(self::Tag::BookContent))) 258 | .chain(events_from_items(&book.book.sections)) 259 | .chain(iter::once(self::Event::End(self::Tag::BookContent))) 260 | .collect(); 261 | Self(events) 262 | } 263 | 264 | /// Create a parser from a `RenderContext`. This is available when using `mdbook` as 265 | /// a binary. 266 | pub fn from_rendercontext(ctx: &'a RenderContext) -> Self { 267 | let config = ConfigParser::new(&ctx.config); 268 | let events = iter::empty() 269 | // Configuration 270 | .chain(iter::once(self::Event::Start(self::Tag::BookConfiguration))) 271 | .chain(iter::once(self::Event::Root(ctx.root.clone()))) 272 | .chain(config) 273 | .chain(iter::once(self::Event::End(self::Tag::BookConfiguration))) 274 | // Content 275 | .chain(iter::once(self::Event::Start(self::Tag::BookContent))) 276 | .chain(events_from_items(&ctx.book.sections)) 277 | .chain(iter::once(self::Event::End(self::Tag::BookContent))) 278 | .collect(); 279 | 280 | Self(events) 281 | } 282 | 283 | pub fn iter(&self) -> impl Iterator> { 284 | self.0.iter() 285 | } 286 | } 287 | 288 | impl<'a> Iterator for Parser<'a> { 289 | type Item = self::Event<'a>; 290 | 291 | fn next(&mut self) -> Option { 292 | self.0.first().cloned() 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /pulldown_typst/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /pulldown_typst/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## 0.3.7 (2024-10-28) 9 | 10 | ### Chore 11 | 12 | - fix formatting. 13 | 14 | ### New Features 15 | 16 | - Add support for typst lines. 17 | 18 | ### Bug Fixes 19 | 20 | - Fix comment now that there are three args. 21 | - Include newline for block quotes. 22 | Found in https://github.com/LegNeato/mdbook-typst/issues/15. 23 | 24 | ### Commit Statistics 25 | 26 | 27 | 28 | - 4 commits contributed to the release. 29 | - 49 days passed between releases. 30 | - 4 commits were understood as [conventional](https://www.conventionalcommits.org). 31 | - 0 issues like '(#ID)' were seen in commit messages 32 | 33 | ### Commit Details 34 | 35 | 36 | 37 |
view details 38 | 39 | * **Uncategorized** 40 | - Add support for typst lines. ([`baf8ccf`](https://github.com/LegNeato/pullup/commit/baf8ccf8c3c0425c62bf9ae943a5e520148437e6)) 41 | - Fix comment now that there are three args. ([`2c3f212`](https://github.com/LegNeato/pullup/commit/2c3f212779c2f3e912dd11e47df135c8e964f658)) 42 | - Fix formatting. ([`f719461`](https://github.com/LegNeato/pullup/commit/f7194610fb257dc67dece3916d243578c864cc3f)) 43 | - Include newline for block quotes. ([`871441f`](https://github.com/LegNeato/pullup/commit/871441f01c9a76b09c83dc28ad016b70eae3247a)) 44 |
45 | 46 | ## 0.3.6 (2024-09-08) 47 | 48 | 49 | 50 | 51 | ### Chore 52 | 53 | - fix version 54 | 55 | ### Bug Fixes 56 | 57 | - Make table markup output work 58 | 59 | ### Other 60 | 61 | - Add support for markdown tables. 62 | 63 | ### Commit Statistics 64 | 65 | 66 | 67 | - 6 commits contributed to the release. 68 | - 188 days passed between releases. 69 | - 3 commits were understood as [conventional](https://www.conventionalcommits.org). 70 | - 2 unique issues were worked on: [#1](https://github.com/LegNeato/pullup/issues/1), [#2](https://github.com/LegNeato/pullup/issues/2) 71 | 72 | ### Commit Details 73 | 74 | 75 | 76 |
view details 77 | 78 | * **[#1](https://github.com/LegNeato/pullup/issues/1)** 79 | - Add support for translating blockquotes. ([`da906a5`](https://github.com/LegNeato/pullup/commit/da906a5c9669b50e4e27f6949f063c579ceccc0e)) 80 | * **[#2](https://github.com/LegNeato/pullup/issues/2)** 81 | - Add support for markdown tables. ([`eb7246d`](https://github.com/LegNeato/pullup/commit/eb7246dc692d7010e8a4eef897500d0daf77581e)) 82 | * **Uncategorized** 83 | - Release pulldown_typst v0.3.6 ([`73e09c5`](https://github.com/LegNeato/pullup/commit/73e09c57e5a3fea330e3e5520b6a211edc2d24e7)) 84 | - Make table markup output work ([`5326955`](https://github.com/LegNeato/pullup/commit/5326955c4f1c00c159f7a8fe13e2c2bdd4bcf56b)) 85 | - Release pulldown_mdbook v0.3.2, pulldown_typst v0.3.5, pullup v0.3.5 ([`cf09463`](https://github.com/LegNeato/pullup/commit/cf09463f47a9a5b498584850c6c094e6904a209a)) 86 | - Fix version ([`92793ad`](https://github.com/LegNeato/pullup/commit/92793adefa23509ec2490934767a7e2922f23742)) 87 |
88 | 89 | ## 0.3.5 (2024-09-08) 90 | 91 | 92 | 93 | 94 | ### Chore 95 | 96 | - fix version 97 | 98 | ### Other 99 | 100 | - Add support for markdown tables. 101 | 102 | ## 0.3.3 (2024-03-03) 103 | 104 | 105 | 106 | 107 | ### Chore 108 | 109 | - changelog for smart release 110 | 111 | ### Chore 112 | 113 | - prepare for release 114 | 115 | ### Commit Statistics 116 | 117 | 118 | 119 | - 5 commits contributed to the release. 120 | - 92 days passed between releases. 121 | - 2 commits were understood as [conventional](https://www.conventionalcommits.org). 122 | - 0 issues like '(#ID)' were seen in commit messages 123 | 124 | ### Commit Details 125 | 126 | 127 | 128 |
view details 129 | 130 | * **Uncategorized** 131 | - Release pulldown_typst v0.3.3, pullup v0.3.3 ([`be1290a`](https://github.com/LegNeato/pullup/commit/be1290a5965bee0792937d0d03fb341250863109)) 132 | - Prepare for release ([`c3f5528`](https://github.com/LegNeato/pullup/commit/c3f552838a6772e127181b969fdac30b3cb4956d)) 133 | - Changelog for smart release ([`8dadae2`](https://github.com/LegNeato/pullup/commit/8dadae2a87c49929bc7e82680d738990f50c9682)) 134 | - Fix clippy ([`eb4551f`](https://github.com/LegNeato/pullup/commit/eb4551ff4f41e24919904937f1a8095a2e952c6b)) 135 | - Fix https://github.com/LegNeato/mdbook-typst/issues/3 ([`338d530`](https://github.com/LegNeato/pullup/commit/338d5306452e336a4d74c288ed0e02017f9793b1)) 136 |
137 | 138 | ## v0.3.2 (2023-12-01) 139 | 140 | 141 | 142 | ### Chore 143 | 144 | - Add tests for emphasis. 145 | 146 | ### Commit Statistics 147 | 148 | 149 | 150 | - 3 commits contributed to the release. 151 | - 1 commit was understood as [conventional](https://www.conventionalcommits.org). 152 | - 0 issues like '(#ID)' were seen in commit messages 153 | 154 | ### Commit Details 155 | 156 | 157 | 158 |
view details 159 | 160 | * **Uncategorized** 161 | - Release pulldown_typst v0.3.2 ([`47f78c1`](https://github.com/LegNeato/pullup/commit/47f78c144c8e6b2f26d7456bf970d6538bccfb82)) 162 | - Add tests for emphasis. ([`bd7fb0c`](https://github.com/LegNeato/pullup/commit/bd7fb0c0c7d5cbe413c5cf9b9c50422a5f2da407)) 163 | - Switch from using typist markup to markup functions. ([`67f4f92`](https://github.com/LegNeato/pullup/commit/67f4f922e2402107bdd4540c6e3bb1ab818b2321)) 164 |
165 | 166 | ## v0.3.1 (2023-12-01) 167 | 168 | ### Bug Fixes 169 | 170 | - try to fix cargo-smart-release. 171 | 172 | ### Commit Statistics 173 | 174 | 175 | 176 | - 15 commits contributed to the release. 177 | - 1 commit was understood as [conventional](https://www.conventionalcommits.org). 178 | - 0 issues like '(#ID)' were seen in commit messages 179 | 180 | ### Commit Details 181 | 182 | 183 | 184 |
view details 185 | 186 | * **Uncategorized** 187 | - Release pulldown_mdbook v0.3.1, pulldown_typst v0.3.1, pullup v0.3.1 ([`e565ece`](https://github.com/LegNeato/pullup/commit/e565ece82bcc04226211f278f0bbbefe7754ff68)) 188 | - Release pulldown_mdbook v0.3.0, pulldown_typst v0.3.0, pullup v0.3.0 ([`2c88246`](https://github.com/LegNeato/pullup/commit/2c88246b29b36560060646dcbccedeb791097c36)) 189 | - Release pulldown_mdbook v0.3.0, pulldown_typst v0.3.0, pullup v0.3.0 ([`8e2d360`](https://github.com/LegNeato/pullup/commit/8e2d36063d64727a04101e29a1c7b7cd231f31f2)) 190 | - Try to fix cargo-smart-release. ([`6f1e1b4`](https://github.com/LegNeato/pullup/commit/6f1e1b495e53fdf1936ccf25f6f3e26ae26e3d20)) 191 | - Adjusting changelogs prior to release of pulldown_mdbook v0.3.0, pulldown_typst v0.3.0, pullup v0.3.0 ([`b0018bf`](https://github.com/LegNeato/pullup/commit/b0018bf5064900690b490ddf8c3647356bce40c7)) 192 | - Add changelogs. ([`e89557b`](https://github.com/LegNeato/pullup/commit/e89557be19304844054a26622c6b1e28987f0937)) 193 | - Fix Typst markup output with special characters in inline code. ([`177e538`](https://github.com/LegNeato/pullup/commit/177e5382581e2e6620df92e2aabe735f9b7b02d0)) 194 | - Add tracing, change some converters. ([`8b2e292`](https://github.com/LegNeato/pullup/commit/8b2e2921fc3a5cf1a3d2ce7a46ddd3867f75479a)) 195 | - Treat config / metadata differently. ([`899887d`](https://github.com/LegNeato/pullup/commit/899887dfc4816f20b8df9375cf6edcbef3c84ce5)) 196 | - Bump versions. ([`3ceaa03`](https://github.com/LegNeato/pullup/commit/3ceaa03661aae8f890d62e3ac90fd4c1e8e55b56)) 197 | - Add typst converters. ([`993b0d5`](https://github.com/LegNeato/pullup/commit/993b0d5a635fc1adf3732d64e72ed02e27ff1f51)) 198 | - Add markdown converters. ([`6656dc3`](https://github.com/LegNeato/pullup/commit/6656dc3df1acce49706c14fc9e12f461fac160c5)) 199 | - Better typst docs. ([`1bf8261`](https://github.com/LegNeato/pullup/commit/1bf8261d352354811329d2649421fd4bd2f26262)) 200 | - Move typst markup generation to its own module. ([`7fbcc64`](https://github.com/LegNeato/pullup/commit/7fbcc6425bbb2e417b8335d8ea133ac7a92c2394)) 201 | - Bring in other crates. ([`1ab5157`](https://github.com/LegNeato/pullup/commit/1ab51574957a2a7c1643145f13c0e13322755861)) 202 |
203 | 204 | ## v0.3.0 (2023-12-01) 205 | 206 | ### Bug Fixes 207 | 208 | - try to fix cargo-smart-release. 209 | 210 | -------------------------------------------------------------------------------- /pulldown_typst/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pulldown_typst" 3 | version = "0.3.7" 4 | description = "A pull parser for Typst markup" 5 | license = "MIT OR Apache-2.0" 6 | edition = "2021" 7 | authors = ["Christian Legnitto "] 8 | 9 | [features] 10 | tracing = ["dep:tracing"] 11 | 12 | [dependencies] 13 | tracing = { version = "0.1.40", optional = true } 14 | # TODO: Remove this, only using for `CowStr`. 15 | pulldown-cmark = { version = "0.9.3", default-features = false } 16 | -------------------------------------------------------------------------------- /pulldown_typst/README.md: -------------------------------------------------------------------------------- 1 | # pulldown_typst 2 | 3 | This library is a pull parser for books created with 4 | [Typst](https://github.com/typst/typst). 5 | 6 | It does not currently do any parsing. -------------------------------------------------------------------------------- /pulldown_typst/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::num::NonZeroU8; 2 | pub mod markup; 3 | // TODO: remove this. 4 | use pulldown_cmark::CowStr; 5 | 6 | #[derive(Debug, PartialEq, Clone)] 7 | pub enum Event<'a> { 8 | /// Start of a tagged element. Events that are yielded after this event 9 | /// and before its corresponding `End` event are inside this element. 10 | /// Start and end events are guaranteed to be balanced. 11 | Start(Tag<'a>), 12 | /// End of a tagged element. 13 | End(Tag<'a>), 14 | /// A text node. 15 | Text(CowStr<'a>), 16 | /// An inline code node. 17 | Code(CowStr<'a>), 18 | /// A soft line break. 19 | Linebreak, 20 | /// A hard line break. 21 | Parbreak, 22 | /// A page break. 23 | PageBreak, 24 | /// A line. The first field is the start point, the second is the end point, the 25 | /// third field is the length, the fourth is the angle, and the fifth is the stroke. 26 | /// 27 | /// See . 28 | // TODO: make this strongly typed. 29 | Line( 30 | // start 31 | Option<(CowStr<'a>, CowStr<'a>)>, 32 | // end 33 | Option<(CowStr<'a>, CowStr<'a>)>, 34 | // length 35 | Option>, 36 | // angle 37 | Option>, 38 | // stroke 39 | Option>, 40 | ), 41 | /// A let binding. First argument is lhs, second is rhs. 42 | /// 43 | /// See . 44 | Let(CowStr<'a>, CowStr<'a>), 45 | /// A function call. The first field is the target variable (without `#`), the 46 | /// second is the function name, and the third is a list of arguments. 47 | /// 48 | /// If calling `document()`, prefer [`DocumentFunctionCall`]. 49 | // TODO: make this strongly typed. 50 | FunctionCall(Option>, CowStr<'a>, Vec>), 51 | /// A `document` function call. The field is the list of arguments. 52 | /// 53 | /// Prefer this over the more general `FunctionCall` as document calls must appear 54 | /// before any content. 55 | /// 56 | /// See . 57 | // TODO: make this strongly typed. 58 | DocumentFunctionCall(Vec>), 59 | /// A set rule. 60 | /// 61 | /// If setting document metadata, prefer [`DocumentSet`]. 62 | /// 63 | /// See . 64 | // TODO: make this a tag. 65 | Set(CowStr<'a>, CowStr<'a>, CowStr<'a>), 66 | /// A `document` set rule. The first field is the parameter name, the second is the 67 | /// parameter value. 68 | /// 69 | /// Prefer this over the more general [`Set`] as document set rules must appear 70 | /// before any content. 71 | /// 72 | /// See . 73 | DocumentSet(CowStr<'a>, CowStr<'a>), 74 | 75 | /// Raw string data what will be bassed through directly to typst. Prefer using 76 | /// other strongly-typed rules. 77 | Raw(CowStr<'a>), 78 | } 79 | 80 | /// Tags for elements that can contain other elements. 81 | #[derive(Clone, Debug, PartialEq)] 82 | pub enum Tag<'a> { 83 | /// A paragraph of text and other inline elements. 84 | Paragraph, 85 | 86 | /// A show rule. 87 | /// 88 | /// See . 89 | Show( 90 | ShowType, 91 | CowStr<'a>, 92 | Option<(CowStr<'a>, CowStr<'a>, CowStr<'a>)>, 93 | Option>, 94 | ), 95 | 96 | /// A heading. The first field indicates the level of the heading, the second if it 97 | /// should be included in outline, and the third if it should be included in 98 | /// bookmarks. 99 | Heading(NonZeroU8, TableOfContents, Bookmarks), 100 | 101 | /// A code block. The first argument is the 102 | /// fenced value if it exists, the second is how it should be displayed. 103 | CodeBlock(Option>, CodeBlockDisplay), 104 | 105 | /// A bullted list. The first field indicates the marker to use, the second is if 106 | /// tight is desired. Contains only list items. 107 | BulletList(Option<&'a str>, bool), 108 | /// A numbered / enumerated list (also called an _enum_ by typst). The first field 109 | /// indicates the starting number, the second is the [numbering 110 | /// pattern](https://typst.app/docs/reference/meta/numbering/), the third is if 111 | /// tight is desired. Contains only list items. 112 | /// 113 | /// See . 114 | NumberedList(u64, Option>, bool), 115 | /// A list item. 116 | Item, 117 | /// A quote. 118 | /// The second argument determines if it should be wrapped in quotes. 119 | /// The third argument is the attribution value if it exists. 120 | /// 121 | /// See . 122 | Quote(QuoteType, QuoteQuotes, Option>), 123 | // Span-level tags 124 | Emphasis, 125 | Strong, 126 | Strikethrough, 127 | 128 | /// A link. The first field is the type and the second is the destination URL. 129 | Link(LinkType, CowStr<'a>), 130 | 131 | /// A table. The first field is the alignment of each column. 132 | Table(Vec), 133 | /// A table header row. Must come after a #[Tag::Table]. 134 | TableHead, 135 | /// A table row. Must come after a #[Tag::Table]. 136 | TableRow, 137 | /// A table row. Must come after a #[Tag::TableRow]. 138 | TableCell, 139 | } 140 | 141 | /// How to display a code block. 142 | #[derive(Clone, Debug, PartialEq)] 143 | pub enum CodeBlockDisplay { 144 | Block, 145 | Inline, 146 | } 147 | 148 | /// Item appearance in bookmarks. 149 | #[derive(Clone, Debug, PartialEq)] 150 | pub enum Bookmarks { 151 | Include, 152 | Exclude, 153 | } 154 | 155 | /// Item appearance in the table of contents. 156 | #[derive(Clone, Debug, PartialEq)] 157 | pub enum TableOfContents { 158 | Include, 159 | Exclude, 160 | } 161 | 162 | /// The pattern to use whren numbering items. 163 | /// 164 | /// See . 165 | #[derive(Clone, Debug, PartialEq)] 166 | pub struct NumberingPattern<'a>(&'a str); 167 | 168 | /// Type specifier for Show rules. See [Tag::Show](enum.Tag.html#variant.Show) for 169 | /// more information. 170 | // TODO: support different dests. 171 | #[derive(Clone, Debug, PartialEq, Copy)] 172 | pub enum ShowType { 173 | ShowSet, 174 | Function, 175 | } 176 | 177 | /// Type specifier for inline links. See [Tag::Link](enum.Tag.html#variant.Link) for 178 | /// more information. 179 | #[derive(Clone, Debug, PartialEq, Copy)] 180 | pub enum LinkType { 181 | /// Link like `#link("https://example.com")` 182 | Url, 183 | /// Link like `#link("https://example.com")[my cool content]` 184 | Content, 185 | /// Autolink like `http://foo.bar/baz`. 186 | Autolink, 187 | } 188 | 189 | /// Type specifier for a quote. 190 | #[derive(Clone, Debug, PartialEq, Copy)] 191 | pub enum QuoteType { 192 | Block, 193 | Inline, 194 | } 195 | 196 | /// Include a quote in quotes. 197 | /// See 198 | #[derive(Clone, Debug, PartialEq, Copy)] 199 | pub enum QuoteQuotes { 200 | WrapInDoubleQuotes, 201 | DoNotWrapInDoubleQuotes, 202 | Auto, 203 | } 204 | 205 | /// Alignment of a table cell. 206 | #[derive(Clone, Debug, PartialEq, Copy)] 207 | pub enum TableCellAlignment { 208 | Left, 209 | Center, 210 | Right, 211 | None, 212 | } 213 | -------------------------------------------------------------------------------- /pulldown_typst/src/markup.rs: -------------------------------------------------------------------------------- 1 | use crate::{Event, LinkType, QuoteQuotes, QuoteType, ShowType, TableCellAlignment, Tag}; 2 | use std::{collections::VecDeque, fmt::Write, io::ErrorKind}; 3 | 4 | fn typst_escape(s: &str) -> String { 5 | s.replace('$', "\\$") 6 | .replace('#', "\\#") 7 | .replace('<', "\\<") 8 | .replace('>', "\\>") 9 | .replace('*', "\\*") 10 | .replace('_', " \\_") 11 | .replace('`', "\\`") 12 | .replace('@', "\\@") 13 | } 14 | 15 | /// Convert Typst events to Typst markup. 16 | /// 17 | /// Note: while each item returned by the iterator is a `String`, items may contain 18 | /// multiple lines. 19 | // TODO: tests 20 | pub struct TypstMarkup<'a, T> { 21 | tag_queue: VecDeque>, 22 | codeblock_queue: VecDeque<()>, 23 | iter: T, 24 | } 25 | 26 | impl<'a, T> TypstMarkup<'a, T> 27 | where 28 | T: Iterator>, 29 | { 30 | pub fn new(iter: T) -> Self { 31 | Self { 32 | tag_queue: VecDeque::new(), 33 | codeblock_queue: VecDeque::new(), 34 | iter, 35 | } 36 | } 37 | } 38 | 39 | impl<'a, T> Iterator for TypstMarkup<'a, T> 40 | where 41 | T: Iterator>, 42 | { 43 | type Item = String; 44 | 45 | fn next(&mut self) -> Option { 46 | match self.iter.next() { 47 | None => None, 48 | Some(Event::Start(x)) => { 49 | let ret = match x { 50 | Tag::Paragraph => Some("#par()[".to_string()), 51 | Tag::Show(ty, ref selector, ref set, ref func) => match ty { 52 | ShowType::ShowSet => { 53 | let (ele, k, v) = set.as_ref().expect("set data for show-set"); 54 | Some( 55 | format!("#show {}: set {}({}:{})", selector, ele, k, v).to_string(), 56 | ) 57 | } 58 | ShowType::Function => Some( 59 | format!( 60 | "#show {}:{}", 61 | selector, 62 | func.as_ref().expect("function body"), 63 | ) 64 | .to_string(), 65 | ), 66 | }, 67 | Tag::Heading(n, _, _) => Some(format!("{} ", "=".repeat(n.get().into()))), 68 | // TODO: get the number of backticks / tildes somehow. 69 | Tag::CodeBlock(ref fence, ref _display) => { 70 | let depth = self.codeblock_queue.len(); 71 | self.codeblock_queue.push_back(()); 72 | Some(format!( 73 | "{}{}\n", 74 | "`".repeat(6 + depth), 75 | fence 76 | .clone() 77 | .map(|x| x.into_string()) 78 | .unwrap_or_else(|| "".to_string()) 79 | )) 80 | } 81 | Tag::BulletList(_, _) => None, 82 | Tag::NumberedList(_, _, _) => None, 83 | Tag::Item => { 84 | let list = self.tag_queue.back().expect("list item contained in list"); 85 | 86 | match list { 87 | Tag::BulletList(_, _) => Some("- ".to_string()), 88 | Tag::NumberedList(_, _, _) => Some("+ ".to_string()), 89 | _ => unreachable!(), 90 | } 91 | } 92 | Tag::Emphasis => Some("#emph[".to_string()), 93 | Tag::Strong => Some("#strong[".to_string()), 94 | Tag::Link(ref ty, ref url) => match ty { 95 | LinkType::Content => Some(format!("#link(\"{url}\")[")), 96 | LinkType::Url | LinkType::Autolink => Some(format!("#link(\"{url}\")[")), 97 | }, 98 | Tag::Quote(ref ty, ref quotes, ref attribution) => { 99 | let block = match ty { 100 | &QuoteType::Block => "block: true,", 101 | &QuoteType::Inline => "block: false,", 102 | }; 103 | let quotes = match quotes { 104 | &QuoteQuotes::DoNotWrapInDoubleQuotes => "quotes: false,", 105 | &QuoteQuotes::WrapInDoubleQuotes => "quotes: true,", 106 | &QuoteQuotes::Auto => "quotes: auto,", 107 | }; 108 | match attribution { 109 | Some(attribution) => Some(format!( 110 | "#quote({} {} attribution: [{}])[", 111 | block, quotes, attribution 112 | )), 113 | None => Some(format!("#quote({} {})[", block, quotes)), 114 | } 115 | } 116 | Tag::Table(ref alignment) => { 117 | let alignments = alignment 118 | .iter() 119 | .map(|a| match a { 120 | TableCellAlignment::Left => "left", 121 | TableCellAlignment::Center => "center", 122 | TableCellAlignment::Right => "right", 123 | TableCellAlignment::None => "none", 124 | }) 125 | .collect::>() 126 | .join(", "); 127 | Some(format!("#table(align: [{}])[\n", alignments)) 128 | } 129 | Tag::TableRow => Some("#row[\n".to_string()), 130 | Tag::TableHead => Some("#row[\n".to_string()), 131 | Tag::TableCell => Some("#cell[".to_string()), 132 | _ => todo!(), 133 | }; 134 | 135 | // Set the current tag for later processing and return optional event. 136 | self.tag_queue.push_back(x); 137 | if ret.is_none() { 138 | return Some("".to_string()); 139 | } 140 | ret 141 | } 142 | Some(Event::End(x)) => { 143 | let ret = match x { 144 | Tag::Paragraph => Some("]\n".to_string()), 145 | Tag::Heading(_, _, _) => Some("\n".to_string()), 146 | Tag::Item => Some("\n".to_string()), 147 | Tag::Emphasis => Some("]".to_string()), 148 | Tag::Strong => Some("]".to_string()), 149 | Tag::BulletList(_, _) => Some("".to_string()), 150 | Tag::NumberedList(_, _, _) => Some("".to_string()), 151 | Tag::CodeBlock(_, _) => { 152 | let _ = self.codeblock_queue.pop_back(); 153 | let depth = self.codeblock_queue.len(); 154 | Some(format!("{}\n", "`".repeat(6 + depth))) 155 | } 156 | Tag::Link(ty, _) => match ty { 157 | LinkType::Content => Some("]".to_string()), 158 | LinkType::Url | LinkType::Autolink => Some("]".to_string()), 159 | }, 160 | Tag::Show(_, _, _, _) => Some("\n".to_string()), 161 | Tag::Quote(quote_type, _, _) => Some(match quote_type { 162 | QuoteType::Inline => "]".to_string(), 163 | QuoteType::Block => "]\n".to_string(), 164 | }), 165 | Tag::Table(_) => Some("]\n".to_string()), 166 | Tag::TableHead => Some("\n]\n".to_string()), 167 | Tag::TableRow => Some("\n]\n".to_string()), 168 | Tag::TableCell => Some("]".to_string()), 169 | _ => todo!(), 170 | }; 171 | 172 | let in_tag = self.tag_queue.pop_back(); 173 | 174 | // Make sure we are in a good state. 175 | assert_eq!(in_tag, Some(x)); 176 | ret 177 | } 178 | Some(Event::Raw(x)) => Some(x.into_string()), 179 | Some(Event::Text(x)) => { 180 | if self.codeblock_queue.is_empty() { 181 | Some(typst_escape(&x)) 182 | } else { 183 | Some(x.into_string()) 184 | } 185 | } 186 | Some(Event::Code(x)) => Some(format!( 187 | "#raw(\"{}\")", 188 | x 189 | // "Raw" still needs forward slashes escaped or they will break out of 190 | // the tag. 191 | .replace('\\', r#"\\"#) 192 | // "Raw" still needs quotes escaped or they will prematurely end the tag. 193 | .replace('"', r#"\""#) 194 | )), 195 | Some(Event::Linebreak) => Some("#linebreak()\n".to_string()), 196 | Some(Event::Parbreak) => Some("#parbreak()\n".to_string()), 197 | Some(Event::PageBreak) => Some("#pagebreak()\n".to_string()), 198 | Some(Event::Line(start, end, length, angle, stroke)) => { 199 | let mut parts = vec![]; 200 | 201 | if let Some(start) = start { 202 | parts.push(format!("start: ({}, {})", start.0, start.1)); 203 | } 204 | if let Some(end) = end { 205 | parts.push(format!("end: ({}, {})", end.0, end.1)); 206 | } 207 | if let Some(length) = length { 208 | parts.push(format!("length: {}", length)); 209 | } 210 | if let Some(angle) = angle { 211 | parts.push(format!("angle: {}", angle)); 212 | } 213 | if let Some(stroke) = stroke { 214 | parts.push(format!("stroke: {}", stroke)); 215 | } 216 | 217 | Some(format!("#line({})\n", parts.join(", "))) 218 | } 219 | Some(Event::Let(lhs, rhs)) => Some(format!("#let {lhs} = {rhs}\n")), 220 | Some(Event::FunctionCall(v, f, args)) => { 221 | let args = args.join(", "); 222 | if let Some(v) = v { 223 | Some(format!("#{v}.{f}({args})\n")) 224 | } else { 225 | Some(format!("#{f}({args})\n")) 226 | } 227 | } 228 | Some(Event::DocumentFunctionCall(args)) => { 229 | let args = args.join(", "); 230 | Some(format!("#document({args})\n")) 231 | } 232 | Some(Event::Set(ele, k, v)) => Some(format!("#set {ele}({k}: {v})\n")), 233 | Some(Event::DocumentSet(k, v)) => Some(format!("#set document({k}: {v})\n")), 234 | } 235 | } 236 | } 237 | 238 | /// Iterate over an Iterator of Typst [`Event`]s, generate Typst markup for each 239 | /// [`Event`], and push it to a `String`. 240 | pub fn push_markup<'a, T>(s: &mut String, iter: T) 241 | where 242 | T: Iterator>, 243 | { 244 | *s = TypstMarkup::new(iter).collect(); 245 | } 246 | 247 | /// Iterate over an Iterator of Typst [`Event`]s, generate Typst markup for each 248 | /// [`Event`], and write it to a `Write`r. 249 | pub fn write_markup<'a, T, W>(w: &mut W, iter: T) -> std::io::Result<()> 250 | where 251 | T: Iterator>, 252 | W: Write, 253 | { 254 | for e in TypstMarkup::new(iter) { 255 | w.write_str(&e) 256 | .map_err(|e| std::io::Error::new(ErrorKind::Other, e))?; 257 | } 258 | Ok(()) 259 | } 260 | 261 | #[cfg(test)] 262 | mod tests { 263 | use super::*; 264 | 265 | mod emphasis { 266 | use super::*; 267 | 268 | #[test] 269 | fn inline() { 270 | let input = vec![ 271 | Event::Start(Tag::Emphasis), 272 | Event::Text("foo bar baz".into()), 273 | Event::End(Tag::Emphasis), 274 | ]; 275 | let output = TypstMarkup::new(input.into_iter()).collect::(); 276 | let expected = "#emph[foo bar baz]"; 277 | assert_eq!(&output, &expected); 278 | } 279 | 280 | #[test] 281 | fn containing_underscores() { 282 | let input = vec![ 283 | Event::Start(Tag::Emphasis), 284 | Event::Text("_whatever_".into()), 285 | Event::End(Tag::Emphasis), 286 | ]; 287 | let output = TypstMarkup::new(input.into_iter()).collect::(); 288 | let expected = "#emph[ \\_whatever \\_]"; 289 | assert_eq!(&output, &expected); 290 | } 291 | 292 | #[test] 293 | fn nested() { 294 | let input = vec![ 295 | Event::Start(Tag::Emphasis), 296 | Event::Start(Tag::Strong), 297 | Event::Text("blah".into()), 298 | Event::End(Tag::Strong), 299 | Event::End(Tag::Emphasis), 300 | ]; 301 | let output = TypstMarkup::new(input.into_iter()).collect::(); 302 | let expected = "#emph[#strong[blah]]"; 303 | assert_eq!(&output, &expected); 304 | } 305 | } 306 | 307 | mod escape { 308 | use super::*; 309 | 310 | #[test] 311 | fn raw_encodes_code() { 312 | let input = vec![Event::Code("*foo*".into())]; 313 | let output = TypstMarkup::new(input.into_iter()).collect::(); 314 | let expected = "#raw(\"*foo*\")"; 315 | assert_eq!(&output, &expected); 316 | } 317 | 318 | #[test] 319 | // https://github.com/LegNeato/mdbook-typst/issues/3 320 | fn raw_escapes_forward_slash() { 321 | let input = vec![Event::Code(r#"\"#.into())]; 322 | let output = TypstMarkup::new(input.into_iter()).collect::(); 323 | let expected = r####"#raw("\\")"####; 324 | assert_eq!(&output, &expected); 325 | 326 | let input = vec![ 327 | Event::Start(Tag::Paragraph), 328 | Event::Text("before ".into()), 329 | Event::Code(r#"\"#.into()), 330 | Event::Text(" after".into()), 331 | Event::End(Tag::Paragraph), 332 | ]; 333 | let output = TypstMarkup::new(input.into_iter()).collect::(); 334 | let expected = r####"#par()[before #raw("\\") after]"####.to_string() + "\n"; 335 | assert_eq!(&output, &expected); 336 | } 337 | 338 | #[test] 339 | fn doesnt_escape_codeblock() { 340 | let input = vec![ 341 | Event::Start(Tag::CodeBlock(None, crate::CodeBlockDisplay::Block)), 342 | Event::Text("*blah*".into()), 343 | Event::End(Tag::CodeBlock(None, crate::CodeBlockDisplay::Block)), 344 | ]; 345 | let output = TypstMarkup::new(input.into_iter()).collect::(); 346 | let expected = "``````\n*blah*``````\n"; 347 | assert_eq!(&output, &expected); 348 | } 349 | 350 | #[test] 351 | fn escapes_link_content() { 352 | let input = vec![ 353 | Event::Start(Tag::Link(LinkType::Content, "http://example.com".into())), 354 | Event::Text("*blah*".into()), 355 | Event::End(Tag::Link(LinkType::Content, "http://example.com".into())), 356 | ]; 357 | let output = TypstMarkup::new(input.into_iter()).collect::(); 358 | let expected = "#link(\"http://example.com\")[\\*blah\\*]"; 359 | assert_eq!(&output, &expected); 360 | } 361 | } 362 | 363 | mod quote { 364 | use super::*; 365 | 366 | #[test] 367 | fn single() { 368 | let input = vec![ 369 | Event::Start(Tag::Quote(QuoteType::Block, QuoteQuotes::Auto, None)), 370 | Event::Text("to be or not to be".into()), 371 | Event::End(Tag::Quote(QuoteType::Block, QuoteQuotes::Auto, None)), 372 | ]; 373 | let output = TypstMarkup::new(input.into_iter()).collect::(); 374 | let expected = "#quote(block: true, quotes: auto,)[to be or not to be]\n"; 375 | assert_eq!(&output, &expected); 376 | } 377 | 378 | #[test] 379 | fn attribution() { 380 | let input = vec![ 381 | Event::Start(Tag::Quote( 382 | QuoteType::Block, 383 | QuoteQuotes::Auto, 384 | Some("some dude".into()), 385 | )), 386 | Event::Text("to be or not to be".into()), 387 | Event::End(Tag::Quote( 388 | QuoteType::Block, 389 | QuoteQuotes::Auto, 390 | Some("some dude".into()), 391 | )), 392 | ]; 393 | let output = TypstMarkup::new(input.into_iter()).collect::(); 394 | let expected = 395 | "#quote(block: true, quotes: auto, attribution: [some dude])[to be or not to be]\n"; 396 | assert_eq!(&output, &expected); 397 | } 398 | 399 | #[test] 400 | fn inline_no_newline() { 401 | let input = vec![ 402 | Event::Start(Tag::Quote( 403 | QuoteType::Inline, 404 | QuoteQuotes::Auto, 405 | Some("some dude".into()), 406 | )), 407 | Event::Text("whatever".into()), 408 | Event::End(Tag::Quote( 409 | QuoteType::Inline, 410 | QuoteQuotes::Auto, 411 | Some("some dude".into()), 412 | )), 413 | ]; 414 | let output = TypstMarkup::new(input.into_iter()).collect::(); 415 | assert!(!output.contains('\n')); 416 | } 417 | 418 | #[test] 419 | fn block_has_newline() { 420 | let input = vec![ 421 | Event::Start(Tag::Quote( 422 | QuoteType::Block, 423 | QuoteQuotes::Auto, 424 | Some("some dude".into()), 425 | )), 426 | Event::Text("whatever".into()), 427 | Event::End(Tag::Quote( 428 | QuoteType::Block, 429 | QuoteQuotes::Auto, 430 | Some("some dude".into()), 431 | )), 432 | ]; 433 | let output = TypstMarkup::new(input.into_iter()).collect::(); 434 | assert!(output.contains('\n')); 435 | } 436 | } 437 | 438 | mod line { 439 | use super::*; 440 | 441 | #[test] 442 | fn basic() { 443 | let input = vec![Event::Line(None, None, None, None, None)]; 444 | let output = TypstMarkup::new(input.into_iter()).collect::(); 445 | let expected = "#line()\n"; 446 | assert_eq!(&output, &expected); 447 | } 448 | 449 | #[test] 450 | fn start() { 451 | let input = vec![Event::Line( 452 | Some(("1".into(), "2".into())), 453 | None, 454 | None, 455 | None, 456 | None, 457 | )]; 458 | let output = TypstMarkup::new(input.into_iter()).collect::(); 459 | let expected = "#line(start: (1, 2))\n"; 460 | assert_eq!(&output, &expected); 461 | } 462 | 463 | #[test] 464 | fn end() { 465 | let input = vec![Event::Line( 466 | None, 467 | Some(("3".into(), "4".into())), 468 | None, 469 | None, 470 | None, 471 | )]; 472 | let output = TypstMarkup::new(input.into_iter()).collect::(); 473 | let expected = "#line(end: (3, 4))\n"; 474 | assert_eq!(&output, &expected); 475 | } 476 | 477 | #[test] 478 | fn length() { 479 | let input = vec![Event::Line(None, None, Some("5".into()), None, None)]; 480 | let output = TypstMarkup::new(input.into_iter()).collect::(); 481 | let expected = "#line(length: 5)\n"; 482 | assert_eq!(&output, &expected); 483 | } 484 | 485 | #[test] 486 | fn angle() { 487 | let input = vec![Event::Line(None, None, None, Some("6".into()), None)]; 488 | let output = TypstMarkup::new(input.into_iter()).collect::(); 489 | let expected = "#line(angle: 6)\n"; 490 | assert_eq!(&output, &expected); 491 | } 492 | 493 | #[test] 494 | fn stroke() { 495 | let input = vec![Event::Line(None, None, None, None, Some("7".into()))]; 496 | let output = TypstMarkup::new(input.into_iter()).collect::(); 497 | let expected = "#line(stroke: 7)\n"; 498 | assert_eq!(&output, &expected); 499 | } 500 | 501 | #[test] 502 | fn all() { 503 | let input = vec![Event::Line( 504 | Some(("1".into(), "2".into())), 505 | Some(("3".into(), "4".into())), 506 | Some("5".into()), 507 | Some("6".into()), 508 | Some("7".into()), 509 | )]; 510 | let output = TypstMarkup::new(input.into_iter()).collect::(); 511 | let expected = "#line(start: (1, 2), end: (3, 4), length: 5, angle: 6, stroke: 7)\n"; 512 | assert_eq!(&output, &expected); 513 | } 514 | } 515 | 516 | #[test] 517 | fn table_conversion() { 518 | let input = vec![ 519 | Event::Start(Tag::Table(vec![ 520 | TableCellAlignment::Left, 521 | TableCellAlignment::Center, 522 | ])), 523 | Event::Start(Tag::TableRow), 524 | Event::Start(Tag::TableCell), 525 | Event::Text("Header 1".into()), 526 | Event::End(Tag::TableCell), 527 | Event::Start(Tag::TableCell), 528 | Event::Text("Header 2".into()), 529 | Event::End(Tag::TableCell), 530 | Event::End(Tag::TableRow), 531 | Event::End(Tag::Table(vec![ 532 | TableCellAlignment::Left, 533 | TableCellAlignment::Center, 534 | ])), 535 | ]; 536 | 537 | let output = TypstMarkup::new(input.into_iter()).collect::(); 538 | let expected = 539 | "#table(align: [left, center])[\n#row[\n#cell[Header 1]#cell[Header 2]\n]\n]\n"; 540 | assert_eq!(output, expected); 541 | } 542 | } 543 | -------------------------------------------------------------------------------- /pullup/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## 0.3.8 (2024-10-28) 9 | 10 | ### Bug Fixes 11 | 12 | - Translate markdown hard breaks to typst linebreaks. 13 | See https://github.com/LegNeato/mdbook-typst/issues/11. 14 | 15 | ### Commit Statistics 16 | 17 | 18 | 19 | - 1 commit contributed to the release. 20 | - 49 days passed between releases. 21 | - 1 commit was understood as [conventional](https://www.conventionalcommits.org). 22 | - 0 issues like '(#ID)' were seen in commit messages 23 | 24 | ### Commit Details 25 | 26 | 27 | 28 |
view details 29 | 30 | * **Uncategorized** 31 | - Translate markdown hard breaks to typst linebreaks. ([`491dc23`](https://github.com/LegNeato/pullup/commit/491dc239a691e4e36bfc5043f533bca50964995c)) 32 |
33 | 34 | ## 0.3.7 (2024-09-08) 35 | 36 | ### Bug Fixes 37 | 38 | - Add tables to builder. 39 | 40 | ### Commit Statistics 41 | 42 | 43 | 44 | - 2 commits contributed to the release. 45 | - 1 commit was understood as [conventional](https://www.conventionalcommits.org). 46 | - 0 issues like '(#ID)' were seen in commit messages 47 | 48 | ### Commit Details 49 | 50 | 51 | 52 |
view details 53 | 54 | * **Uncategorized** 55 | - Release pullup v0.3.7 ([`09478a6`](https://github.com/LegNeato/pullup/commit/09478a6127c682ac3cdc16a17bf15e953571078b)) 56 | - Add tables to builder. ([`b260901`](https://github.com/LegNeato/pullup/commit/b260901620ab890463a47adee4db90977466bb83)) 57 |
58 | 59 | ## 0.3.6 (2024-09-08) 60 | 61 | 62 | 63 | 64 | ### Chore 65 | 66 | - update version 67 | 68 | ### Other 69 | 70 | - Add support for markdown tables. 71 | 72 | ### Commit Statistics 73 | 74 | 75 | 76 | - 6 commits contributed to the release. 77 | - 188 days passed between releases. 78 | - 2 commits were understood as [conventional](https://www.conventionalcommits.org). 79 | - 2 unique issues were worked on: [#1](https://github.com/LegNeato/pullup/issues/1), [#2](https://github.com/LegNeato/pullup/issues/2) 80 | 81 | ### Commit Details 82 | 83 | 84 | 85 |
view details 86 | 87 | * **[#1](https://github.com/LegNeato/pullup/issues/1)** 88 | - Add support for translating blockquotes. ([`da906a5`](https://github.com/LegNeato/pullup/commit/da906a5c9669b50e4e27f6949f063c579ceccc0e)) 89 | * **[#2](https://github.com/LegNeato/pullup/issues/2)** 90 | - Add support for markdown tables. ([`eb7246d`](https://github.com/LegNeato/pullup/commit/eb7246dc692d7010e8a4eef897500d0daf77581e)) 91 | * **Uncategorized** 92 | - Release pullup v0.3.6 ([`f560fe2`](https://github.com/LegNeato/pullup/commit/f560fe2813d706690614375abc84b4c806d91bc1)) 93 | - Release pulldown_typst v0.3.6 ([`73e09c5`](https://github.com/LegNeato/pullup/commit/73e09c57e5a3fea330e3e5520b6a211edc2d24e7)) 94 | - Release pulldown_mdbook v0.3.2, pulldown_typst v0.3.5, pullup v0.3.5 ([`cf09463`](https://github.com/LegNeato/pullup/commit/cf09463f47a9a5b498584850c6c094e6904a209a)) 95 | - Update version ([`7c42018`](https://github.com/LegNeato/pullup/commit/7c4201882d4c31addae88ca016f68b2800c3231f)) 96 |
97 | 98 | ## 0.3.5 (2024-09-08) 99 | 100 | 101 | 102 | 103 | ### Chore 104 | 105 | - update version 106 | 107 | ### Other 108 | 109 | - Add support for markdown tables. 110 | 111 | ## 0.3.3 (2024-03-03) 112 | 113 | 114 | 115 | ### Chore 116 | 117 | - prepare for release 118 | 119 | ### Commit Statistics 120 | 121 | 122 | 123 | - 4 commits contributed to the release. 124 | - 92 days passed between releases. 125 | - 1 commit was understood as [conventional](https://www.conventionalcommits.org). 126 | - 0 issues like '(#ID)' were seen in commit messages 127 | 128 | ### Commit Details 129 | 130 | 131 | 132 |
view details 133 | 134 | * **Uncategorized** 135 | - Release pulldown_typst v0.3.3, pullup v0.3.3 ([`be1290a`](https://github.com/LegNeato/pullup/commit/be1290a5965bee0792937d0d03fb341250863109)) 136 | - Prepare for release ([`c3f5528`](https://github.com/LegNeato/pullup/commit/c3f552838a6772e127181b969fdac30b3cb4956d)) 137 | - More clippy ([`8299b31`](https://github.com/LegNeato/pullup/commit/8299b3118258ed502a965be8209b1fe52d5c1ba0)) 138 | - Fix https://github.com/LegNeato/mdbook-typst/issues/3 ([`338d530`](https://github.com/LegNeato/pullup/commit/338d5306452e336a4d74c288ed0e02017f9793b1)) 139 |
140 | 141 | ## 0.3.2 (2023-12-01) 142 | 143 | ### Bug Fixes 144 | 145 | - Bump pulldown_typst for emphasis and strong fix. 146 | 147 | ### Commit Statistics 148 | 149 | 150 | 151 | - 3 commits contributed to the release. 152 | - 1 commit was understood as [conventional](https://www.conventionalcommits.org). 153 | - 0 issues like '(#ID)' were seen in commit messages 154 | 155 | ### Commit Details 156 | 157 | 158 | 159 |
view details 160 | 161 | * **Uncategorized** 162 | - Release pullup v0.3.2 ([`d43b8c4`](https://github.com/LegNeato/pullup/commit/d43b8c407a1a64282cb7dc23b944115ed5cfde8e)) 163 | - Bump pulldown_typst for emphasis and strong fix. ([`2a3237b`](https://github.com/LegNeato/pullup/commit/2a3237b61950fd9b7158aa29bb330d1f9254ff84)) 164 | - Release pulldown_typst v0.3.2 ([`47f78c1`](https://github.com/LegNeato/pullup/commit/47f78c144c8e6b2f26d7456bf970d6538bccfb82)) 165 |
166 | 167 | ## v0.3.1 (2023-12-01) 168 | 169 | ### Bug Fixes 170 | 171 | - try to fix cargo-smart-release. 172 | 173 | ### Commit Statistics 174 | 175 | 176 | 177 | - 22 commits contributed to the release. 178 | - 1 commit was understood as [conventional](https://www.conventionalcommits.org). 179 | - 0 issues like '(#ID)' were seen in commit messages 180 | 181 | ### Commit Details 182 | 183 | 184 | 185 |
view details 186 | 187 | * **Uncategorized** 188 | - Release pulldown_mdbook v0.3.1, pulldown_typst v0.3.1, pullup v0.3.1 ([`e565ece`](https://github.com/LegNeato/pullup/commit/e565ece82bcc04226211f278f0bbbefe7754ff68)) 189 | - Release pulldown_mdbook v0.3.0, pulldown_typst v0.3.0, pullup v0.3.0 ([`2c88246`](https://github.com/LegNeato/pullup/commit/2c88246b29b36560060646dcbccedeb791097c36)) 190 | - Release pulldown_mdbook v0.3.0, pulldown_typst v0.3.0, pullup v0.3.0 ([`8e2d360`](https://github.com/LegNeato/pullup/commit/8e2d36063d64727a04101e29a1c7b7cd231f31f2)) 191 | - Try to fix cargo-smart-release. ([`6f1e1b4`](https://github.com/LegNeato/pullup/commit/6f1e1b495e53fdf1936ccf25f6f3e26ae26e3d20)) 192 | - Adjusting changelogs prior to release of pulldown_mdbook v0.3.0, pulldown_typst v0.3.0, pullup v0.3.0 ([`b0018bf`](https://github.com/LegNeato/pullup/commit/b0018bf5064900690b490ddf8c3647356bce40c7)) 193 | - Add changelogs. ([`e89557b`](https://github.com/LegNeato/pullup/commit/e89557be19304844054a26622c6b1e28987f0937)) 194 | - Adjusting changelogs prior to release of pulldown_mdbook v0.3.0, pulldown_typst v0.3.0, pullup v0.3.0 ([`230eb15`](https://github.com/LegNeato/pullup/commit/230eb15f4eaefdc262db59e7946c60ac7e209b76)) 195 | - Fix Typst markup output with special characters in inline code. ([`177e538`](https://github.com/LegNeato/pullup/commit/177e5382581e2e6620df92e2aabe735f9b7b02d0)) 196 | - Add tracing, change some converters. ([`8b2e292`](https://github.com/LegNeato/pullup/commit/8b2e2921fc3a5cf1a3d2ce7a46ddd3867f75479a)) 197 | - Make converters take self. ([`64791a0`](https://github.com/LegNeato/pullup/commit/64791a0a011f57aba5fba6e4fcb8347e5a4423d8)) 198 | - Treat config / metadata differently. ([`899887d`](https://github.com/LegNeato/pullup/commit/899887dfc4816f20b8df9375cf6edcbef3c84ce5)) 199 | - Bump versions. ([`3ceaa03`](https://github.com/LegNeato/pullup/commit/3ceaa03661aae8f890d62e3ac90fd4c1e8e55b56)) 200 | - Add mdbook parser and make docs consistent. ([`91b4f88`](https://github.com/LegNeato/pullup/commit/91b4f88596430ffd2560a216e40080f89a38697c)) 201 | - Add mdbook to typst builder. ([`babd598`](https://github.com/LegNeato/pullup/commit/babd598fc48ed9bb19712a7312c7208253254686)) 202 | - Add docs for converter macro. ([`a7e8048`](https://github.com/LegNeato/pullup/commit/a7e8048a342557fc91a95fd1214ce8eb99124b7a)) 203 | - Add typst converters. ([`993b0d5`](https://github.com/LegNeato/pullup/commit/993b0d5a635fc1adf3732d64e72ed02e27ff1f51)) 204 | - Move typst to a directory. ([`ec8b9f2`](https://github.com/LegNeato/pullup/commit/ec8b9f2abd0ee06d7a3054e48a7ff6037c06f0f6)) 205 | - Remove unused imports. ([`775c5f5`](https://github.com/LegNeato/pullup/commit/775c5f5f219dc88f8d123ab6fde2eae093212dc7)) 206 | - Add markdown converters. ([`6656dc3`](https://github.com/LegNeato/pullup/commit/6656dc3df1acce49706c14fc9e12f461fac160c5)) 207 | - Add conversions from mdbook to typst. ([`1a210ed`](https://github.com/LegNeato/pullup/commit/1a210edb39696b17c44d4459f3403fda85c0a80a)) 208 | - Add README. ([`6038701`](https://github.com/LegNeato/pullup/commit/60387019aa240ba17542fe8d6eeea33294f217e0)) 209 | - Bring in other crates. ([`1ab5157`](https://github.com/LegNeato/pullup/commit/1ab51574957a2a7c1643145f13c0e13322755861)) 210 |
211 | 212 | ## v0.3.0 (2023-12-01) 213 | 214 | ### Bug Fixes 215 | 216 | - try to fix cargo-smart-release. 217 | 218 | -------------------------------------------------------------------------------- /pullup/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pullup" 3 | description = "Convert between markup formats" 4 | license = "MIT OR Apache-2.0" 5 | authors = ["Christian Legnitto "] 6 | version = "0.3.8" 7 | edition = "2021" 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | [features] 11 | markdown = ["dep:pulldown-cmark"] 12 | # TODO: Make this work without the markdown feature. 13 | mdbook = ["dep:pulldown_mdbook", "markdown"] 14 | typst = ["dep:pulldown_typst"] 15 | builder = ["dep:typed-builder"] 16 | tracing = ["dep:tracing", "pulldown_typst/tracing", "pulldown_mdbook/tracing"] 17 | 18 | [dependencies] 19 | pulldown-cmark = { version = "0.9.2", optional = true } 20 | pulldown_mdbook = { version = "^0.3.2", path = "../pulldown_mdbook", optional = true } 21 | pulldown_typst = { version = "^0.3.7", path = "../pulldown_typst", optional = true } 22 | tracing = { version = "0.1.40", optional = true } 23 | typed-builder = { version = "0.18.0", optional = true } 24 | 25 | [dev-dependencies] 26 | similar-asserts = "1.5.0" 27 | 28 | [package.metadata.docs.rs] 29 | all-features = true 30 | rustdoc-args = ["--cfg", "doc_cfg", "--generate-link-to-definition"] 31 | -------------------------------------------------------------------------------- /pullup/README.md: -------------------------------------------------------------------------------- 1 | # Pullup 2 | 3 | **Pullup** converts between [*pull*down 4 | parser](https://github.com/raphlinus/pulldown-cmark#why-a-pull-parser) events for 5 | various mark*up* formats. 6 | 7 | Currently supported markup formats: 8 | 9 | - [Markdown](https://commonmark.org/) (via the `markdown` feature) 10 | - [mdBook](https://github.com/rust-lang/mdBook) (via the `mdbook` feature) 11 | - [Typst](https://github.com/typst/typst) (via the `typst` feature) 12 | 13 | Formats are disabled by default and must be enabled via features before use. 14 | 15 | ## How to use the crate 16 | 17 | 1. Parse markup with a format-specific pulldown parser (for example, 18 | [`pulldown_cmark`](https://github.com/raphlinus/pulldown-cmark) is used to parse 19 | Markdown). The parser creates an iterator of markup-specific `Event`s. 20 | 2. Load the format-specific `Event`s into the multi-format `ParserEvent` provided by 21 | this crate. 22 | - Iterator adaptors to do so are available in the `assert` module. 23 | 3. Operate on the `ParserEvent`s. 24 | 4. Strip irrelevant `ParserEvents` and output to a different format. 25 | -------------------------------------------------------------------------------- /pullup/src/assert.rs: -------------------------------------------------------------------------------- 1 | //! Iterator adaptors that convert markup-specific iterators into iterators that emit 2 | //! [`ParserEvent`](crate::ParserEvent). 3 | 4 | #[cfg(feature = "markdown")] 5 | pub use crate::markdown::AssertMarkdown; 6 | #[cfg(feature = "mdbook")] 7 | pub use crate::mdbook::AssertMdbook; 8 | #[cfg(feature = "typst")] 9 | pub use crate::typst::AssertTypst; 10 | -------------------------------------------------------------------------------- /pullup/src/filter.rs: -------------------------------------------------------------------------------- 1 | //! Iterator adaptors that convert [`ParserEvent`](crate::ParserEvent) iterators to 2 | //! markup-specific iterators. 3 | 4 | #[cfg(feature = "markdown")] 5 | pub use crate::markdown::MarkdownFilter; 6 | #[cfg(feature = "mdbook")] 7 | pub use crate::mdbook::MdbookFilter; 8 | #[cfg(feature = "typst")] 9 | pub use crate::typst::TypstFilter; 10 | -------------------------------------------------------------------------------- /pullup/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod assert; 2 | pub mod filter; 3 | 4 | #[cfg(feature = "markdown")] 5 | pub mod markdown; 6 | #[cfg(feature = "mdbook")] 7 | pub mod mdbook; 8 | #[cfg(feature = "typst")] 9 | pub mod typst; 10 | 11 | /// Represents all the types of markup events this crate can operate on. 12 | /// 13 | /// Markup adapters: 14 | /// * Convert format-specific iterators to [`ParserEvent`] iterators for further 15 | /// processing. 16 | /// * Convert [`ParserEvent`] iterators to format-specific iterators for output. 17 | #[derive(Debug, Clone, PartialEq)] 18 | pub enum ParserEvent<'a> { 19 | #[cfg(feature = "markdown")] 20 | Markdown(markdown::Event<'a>), 21 | #[cfg(feature = "mdbook")] 22 | Mdbook(mdbook::Event<'a>), 23 | #[cfg(feature = "typst")] 24 | Typst(typst::Event<'a>), 25 | #[cfg(not(any(feature = "markdown", feature = "mdbook", feature = "typst")))] 26 | NoFeaturesEnabled(core::marker::PhantomData<&'a ()>), 27 | } 28 | 29 | #[macro_export] 30 | /// Convert between markup events without a buffer or lookahead. 31 | macro_rules! converter { 32 | ( 33 | $(#[$attr:meta])* 34 | $struct_name:ident, 35 | $in:ty => $out:ty, 36 | $body:expr 37 | ) => { 38 | // Define the struct with the given name 39 | #[derive(Debug, Clone)] 40 | $(#[$attr])* 41 | pub struct $struct_name<'a, I> { 42 | iter: I, 43 | p: core::marker::PhantomData<&'a ()>, 44 | } 45 | 46 | // Define an implementation for the struct 47 | impl<'a, I> $struct_name<'a, I> 48 | where 49 | I: Iterator>, 50 | { 51 | #[allow(dead_code)] 52 | pub fn new(iter: I) -> Self { 53 | Self { 54 | iter, 55 | p: core::marker::PhantomData, 56 | } 57 | } 58 | } 59 | 60 | impl<'a, I> Iterator for $struct_name<'a, I> 61 | where 62 | I: Iterator, 63 | { 64 | type Item = $out; 65 | 66 | fn next(&mut self) -> Option { 67 | #[cfg(feature = "tracing")] 68 | let span = tracing::span!(tracing::Level::TRACE, &"next"); 69 | #[cfg(feature = "tracing")] 70 | let _enter = span.enter(); 71 | #[allow(clippy::redundant_closure_call)] 72 | $body(self) 73 | } 74 | } 75 | }; 76 | } 77 | -------------------------------------------------------------------------------- /pullup/src/markdown/mod.rs: -------------------------------------------------------------------------------- 1 | //! Support for [Markdown](https://commonmark.org/). 2 | 3 | use crate::ParserEvent; 4 | pub use pulldown_cmark::*; 5 | 6 | pub mod strip; 7 | pub mod to; 8 | 9 | /// Assert that an iterator only contains Markdown events. Panics if another type of 10 | /// event is encountered. 11 | /// 12 | /// For a non-panic version, see [`MarkdownFilter`]. 13 | pub struct AssertMarkdown(pub T); 14 | impl<'a, T> Iterator for AssertMarkdown 15 | where 16 | T: Iterator>, 17 | { 18 | type Item = self::Event<'a>; 19 | 20 | fn next(&mut self) -> Option { 21 | match self.0.next() { 22 | None => None, 23 | Some(ParserEvent::Markdown(x)) => Some(x), 24 | #[cfg(feature = "mdbook")] 25 | Some(ParserEvent::Mdbook(x)) => panic!("unexpected mdbook event: {x:?}"), 26 | #[cfg(feature = "typst")] 27 | Some(ParserEvent::Typst(x)) => panic!("unexpected typst event: {x:?}"), 28 | } 29 | } 30 | } 31 | 32 | /// An iterator that only contains Markdown events. Other types of events will be 33 | /// filtered out. 34 | /// 35 | /// To panic when a non-markdown event is encountered, see [`AssertMarkdown`]. 36 | pub struct MarkdownFilter(pub T); 37 | impl<'a, T> Iterator for MarkdownFilter 38 | where 39 | T: Iterator>, 40 | { 41 | type Item = Event<'a>; 42 | 43 | fn next(&mut self) -> Option { 44 | match self.0.next() { 45 | None => None, 46 | Some(ParserEvent::Markdown(x)) => Some(x), 47 | #[cfg(feature = "mdbook")] 48 | Some(ParserEvent::Mdbook(_)) => self.next(), 49 | #[cfg(feature = "typst")] 50 | Some(ParserEvent::Typst(_)) => self.next(), 51 | } 52 | } 53 | } 54 | 55 | /// An adaptor for events from a Markdown parser. 56 | pub struct MarkdownIter(pub T); 57 | 58 | impl<'a, T> Iterator for MarkdownIter 59 | where 60 | T: Iterator>, 61 | { 62 | type Item = ParserEvent<'a>; 63 | 64 | fn next(&mut self) -> Option { 65 | self.0.next().map(ParserEvent::Markdown) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /pullup/src/markdown/strip.rs: -------------------------------------------------------------------------------- 1 | use crate::converter; 2 | use crate::markdown; 3 | use crate::ParserEvent; 4 | 5 | converter!( 6 | /// Strip out Markdown HTML. 7 | StripHtml, 8 | ParserEvent<'a> => ParserEvent<'a>, 9 | |this: &mut Self| { 10 | match this.iter.next() { 11 | Some(ParserEvent::Markdown(markdown::Event::Html(_))) => { 12 | this.iter.find(|x| !matches!(x, ParserEvent::Markdown(markdown::Event::Html(_)))) 13 | }, 14 | x => x, 15 | } 16 | }); 17 | 18 | #[cfg(test)] 19 | mod tests { 20 | use super::*; 21 | use crate::markdown::CowStr; 22 | use crate::markdown::*; 23 | use similar_asserts::assert_eq; 24 | 25 | // Set up type names so they are clearer and more succint. 26 | use crate::markdown::Event as MdEvent; 27 | use crate::markdown::Tag as MdTag; 28 | 29 | /// Markdown docs: 30 | /// * https://spec.commonmark.org/0.30/#html-blocks 31 | mod html { 32 | use super::*; 33 | #[test] 34 | fn strip_html() { 35 | let md = "\ 36 | # Hello 37 | 38 | 39 | is anybody in there? 40 | 41 | 42 | *just nod if you can hear me* 43 | *foo* 44 | "; 45 | let i = AssertMarkdown(super::StripHtml::new(MarkdownIter(Parser::new(&md)))); 46 | self::assert_eq!( 47 | i.collect::>(), 48 | vec![ 49 | MdEvent::Start(MdTag::Heading(HeadingLevel::H1, None, vec![])), 50 | MdEvent::Text(CowStr::Borrowed("Hello")), 51 | MdEvent::End(MdTag::Heading(HeadingLevel::H1, None, vec![])), 52 | MdEvent::Start(MdTag::Paragraph), 53 | MdEvent::Start(MdTag::Emphasis), 54 | MdEvent::Text(CowStr::Borrowed("just nod if you can hear me")), 55 | MdEvent::End(MdTag::Emphasis), 56 | MdEvent::SoftBreak, 57 | MdEvent::Start(MdTag::Emphasis), 58 | MdEvent::Text(CowStr::Borrowed("foo")), 59 | MdEvent::End(MdTag::Emphasis), 60 | MdEvent::End(MdTag::Paragraph), 61 | ] 62 | ); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /pullup/src/markdown/to/mod.rs: -------------------------------------------------------------------------------- 1 | //! Convert Markdown _to_ other formats. 2 | 3 | #[cfg(feature = "typst")] 4 | pub mod typst; 5 | -------------------------------------------------------------------------------- /pullup/src/markdown/to/typst.rs: -------------------------------------------------------------------------------- 1 | //! Convert Markdown to Typst. 2 | use std::collections::VecDeque; 3 | 4 | use crate::converter; 5 | use crate::markdown; 6 | use crate::typst; 7 | use crate::ParserEvent; 8 | 9 | converter!( 10 | /// Convert Markdown paragraphs to Typst paragraphs. 11 | ConvertParagraphs, 12 | ParserEvent<'a> => ParserEvent<'a>, 13 | |this: &mut Self| { 14 | match this.iter.next() { 15 | Some(ParserEvent::Markdown(markdown::Event::Start(markdown::Tag::Paragraph))) => { 16 | Some(ParserEvent::Typst(typst::Event::Start(typst::Tag::Paragraph))) 17 | }, 18 | Some(ParserEvent::Markdown(markdown::Event::End(markdown::Tag::Paragraph))) => { 19 | Some(ParserEvent::Typst(typst::Event::End(typst::Tag::Paragraph))) 20 | }, 21 | x => x, 22 | } 23 | }); 24 | 25 | /// Convert Markdown text to Typst text. 26 | pub struct ConvertText { 27 | code: VecDeque<()>, 28 | iter: T, 29 | } 30 | 31 | impl<'a, T> ConvertText 32 | where 33 | T: Iterator>, 34 | { 35 | pub fn new(iter: T) -> Self { 36 | ConvertText { 37 | code: VecDeque::new(), 38 | iter, 39 | } 40 | } 41 | } 42 | 43 | impl<'a, T> Iterator for ConvertText 44 | where 45 | T: Iterator>, 46 | { 47 | type Item = ParserEvent<'a>; 48 | 49 | fn next(&mut self) -> Option { 50 | match (self.code.pop_back(), self.iter.next()) { 51 | // In code, include the unescaped text. 52 | (Some(_), Some(ParserEvent::Markdown(markdown::Event::Text(t)))) => { 53 | Some(ParserEvent::Typst(typst::Event::Text(t))) 54 | } 55 | // Not in code, escape the text using typist escaping rules. 56 | (None, Some(ParserEvent::Markdown(markdown::Event::Text(t)))) => { 57 | if t.trim().starts_with("\\[") && t.trim().ends_with("\\]") { 58 | // Strip out mdbook's non-standard MathJax. 59 | // TODO: Translate to typst math and/or expose this as a typed 60 | // markdown event. 61 | self.next() 62 | } else { 63 | Some(ParserEvent::Typst(typst::Event::Text(t))) 64 | } 65 | } 66 | // Track code start. 67 | ( 68 | _, 69 | event @ Some(ParserEvent::Markdown(markdown::Event::Start( 70 | markdown::Tag::CodeBlock(_), 71 | ))), 72 | ) => { 73 | self.code.push_back(()); 74 | event 75 | } 76 | // Track code end. 77 | ( 78 | _, 79 | event @ Some(ParserEvent::Markdown(markdown::Event::End(markdown::Tag::CodeBlock( 80 | _, 81 | )))), 82 | ) => { 83 | let _ = self.code.pop_back(); 84 | event 85 | } 86 | (_, x) => x, 87 | } 88 | } 89 | } 90 | 91 | converter!( 92 | /// Convert Markdown links to Typst links. 93 | ConvertLinks, 94 | ParserEvent<'a> => ParserEvent<'a>, 95 | |this: &mut Self| { 96 | match this.iter.next() { 97 | Some(ParserEvent::Markdown(markdown::Event::Start(markdown::Tag::Link(kind, url, _)))) => { 98 | match kind { 99 | markdown::LinkType::Inline => Some(ParserEvent::Typst(typst::Event::Start(typst::Tag::Link(typst::LinkType::Content, url)))), 100 | /* 101 | markdown::LinkType::Reference => unimplemented!(), 102 | markdown::LinkType::ReferenceUnknown => unimplemented!(), 103 | markdown::LinkType::Collapsed => unimplemented!(), 104 | markdown::LinkType::CollapsedUnknown => unimplemented!(), 105 | markdown::LinkType::Shortcut => unimplemented!(), 106 | markdown::LinkType::ShortcutUnknown => unimplemented!(), 107 | */ 108 | markdown::LinkType::Autolink => { 109 | Some(ParserEvent::Typst(typst::Event::Start(typst::Tag::Link(typst::LinkType::Autolink, url)))) 110 | }, 111 | markdown::LinkType::Email => { 112 | let url = "mailto:".to_string() + url.as_ref(); 113 | Some(ParserEvent::Typst(typst::Event::Start(typst::Tag::Link(typst::LinkType::Url, url.into())))) 114 | }, 115 | _ => this.iter.next() 116 | } 117 | }, 118 | Some(ParserEvent::Markdown(markdown::Event::End(markdown::Tag::Link(kind, url, _)))) => { 119 | match kind { 120 | markdown::LinkType::Inline => Some(ParserEvent::Typst(typst::Event::End(typst::Tag::Link(typst::LinkType::Content, url)))), 121 | /* 122 | markdown::LinkType::Reference => unimplemented!(), 123 | markdown::LinkType::ReferenceUnknown => unimplemented!(), 124 | markdown::LinkType::Collapsed => unimplemented!(), 125 | markdown::LinkType::CollapsedUnknown => unimplemented!(), 126 | markdown::LinkType::Shortcut => unimplemented!(), 127 | markdown::LinkType::ShortcutUnknown => unimplemented!(), 128 | */ 129 | markdown::LinkType::Autolink => { 130 | Some(ParserEvent::Typst(typst::Event::End(typst::Tag::Link(typst::LinkType::Autolink, url)))) 131 | }, 132 | markdown::LinkType::Email => { 133 | let url = "mailto:".to_string() + url.as_ref(); 134 | Some(ParserEvent::Typst(typst::Event::End(typst::Tag::Link(typst::LinkType::Url, url.into())))) 135 | }, 136 | _ => this.iter.next() 137 | } 138 | }, 139 | x => x, 140 | } 141 | }); 142 | 143 | converter!( 144 | /// Convert Markdown **strong** tags to Typst strong tags. 145 | ConvertStrong, 146 | ParserEvent<'a> => ParserEvent<'a>, 147 | |this: &mut Self| { 148 | match this.iter.next() { 149 | Some(ParserEvent::Markdown(markdown::Event::Start(markdown::Tag::Strong))) => { 150 | Some(ParserEvent::Typst(typst::Event::Start(typst::Tag::Strong))) 151 | }, 152 | Some(ParserEvent::Markdown(markdown::Event::End(markdown::Tag::Strong))) => { 153 | Some(ParserEvent::Typst(typst::Event::End(typst::Tag::Strong))) 154 | }, 155 | x => x, 156 | } 157 | }); 158 | 159 | converter!( 160 | /// Convert Markdown _emphasis_ tags to Typst emphasis tags. 161 | ConvertEmphasis, 162 | ParserEvent<'a> => ParserEvent<'a>, 163 | |this: &mut Self| { 164 | match this.iter.next() { 165 | Some(ParserEvent::Markdown(markdown::Event::Start(markdown::Tag::Emphasis))) => { 166 | Some(ParserEvent::Typst(typst::Event::Start(typst::Tag::Emphasis))) 167 | }, 168 | Some(ParserEvent::Markdown(markdown::Event::End(markdown::Tag::Emphasis))) => { 169 | Some(ParserEvent::Typst(typst::Event::End(typst::Tag::Emphasis))) 170 | }, 171 | x => x, 172 | } 173 | }); 174 | 175 | converter!( 176 | /// Convert Markdown soft breaks to Typst line breaks. 177 | ConvertSoftBreaks, 178 | ParserEvent<'a> => ParserEvent<'a>, 179 | |this: &mut Self| { 180 | match this.iter.next() { 181 | Some(ParserEvent::Markdown(markdown::Event::SoftBreak)) => { 182 | Some(ParserEvent::Typst(typst::Event::Text(" ".into()))) 183 | }, 184 | x => x, 185 | } 186 | }); 187 | 188 | converter!( 189 | /// Convert Markdown hard breaks to Typst line breaks. 190 | ConvertHardBreaks, 191 | ParserEvent<'a> => ParserEvent<'a>, 192 | |this: &mut Self| { 193 | match this.iter.next() { 194 | Some(ParserEvent::Markdown(markdown::Event::HardBreak)) => { 195 | Some(ParserEvent::Typst(typst::Event::Linebreak)) 196 | }, 197 | x => x, 198 | } 199 | }); 200 | 201 | converter!( 202 | /// Convert Markdown blockquotes to Typst quotes. 203 | ConvertBlockQuotes, 204 | ParserEvent<'a> => ParserEvent<'a>, 205 | |this: &mut Self| { 206 | match this.iter.next() { 207 | Some(ParserEvent::Markdown(markdown::Event::Start(markdown::Tag::BlockQuote))) => { 208 | Some(ParserEvent::Typst(typst::Event::Start(typst::Tag::Quote(typst::QuoteType::Block, typst::QuoteQuotes::Auto, None)))) 209 | }, 210 | Some(ParserEvent::Markdown(markdown::Event::End(markdown::Tag::BlockQuote))) => { 211 | Some(ParserEvent::Typst(typst::Event::End(typst::Tag::Quote(typst::QuoteType::Block, typst::QuoteQuotes::Auto, None)))) 212 | }, 213 | x => x, 214 | } 215 | }); 216 | 217 | converter!( 218 | /// Convert Markdown code tags to Typst raw tags. 219 | ConvertCode, 220 | ParserEvent<'a> => ParserEvent<'a>, 221 | |this: &mut Self| { 222 | match this.iter.next() { 223 | // Inline. 224 | Some(ParserEvent::Markdown(markdown::Event::Code(x))) => { 225 | Some(ParserEvent::Typst(typst::Event::Code(x))) 226 | }, 227 | // Block. 228 | Some(ParserEvent::Markdown(markdown::Event::Start(markdown::Tag::CodeBlock(kind)))) => { 229 | match kind { 230 | markdown::CodeBlockKind::Indented => Some(ParserEvent::Typst(typst::Event::Start(typst::Tag::CodeBlock(None, typst::CodeBlockDisplay::Block)))), 231 | markdown::CodeBlockKind::Fenced(val) => { 232 | let val = if val.as_ref() == "" { 233 | None 234 | } else { 235 | Some(val) 236 | }; 237 | Some(ParserEvent::Typst(typst::Event::Start(typst::Tag::CodeBlock(val, typst::CodeBlockDisplay::Block)))) 238 | }, 239 | } 240 | }, 241 | Some(ParserEvent::Markdown(markdown::Event::End(markdown::Tag::CodeBlock(kind)))) => { 242 | match kind { 243 | markdown::CodeBlockKind::Indented => Some(ParserEvent::Typst(typst::Event::End(typst::Tag::CodeBlock(None, typst::CodeBlockDisplay::Block)))), 244 | markdown::CodeBlockKind::Fenced(val) => { 245 | let val = if val.as_ref() == "" { 246 | None 247 | } else { 248 | Some(val) 249 | }; 250 | Some(ParserEvent::Typst(typst::Event::End(typst::Tag::CodeBlock(val, typst::CodeBlockDisplay::Block)))) 251 | }, 252 | } 253 | }, 254 | x => x, 255 | } 256 | }); 257 | 258 | converter!( 259 | /// Convert Markdown lists to Typst lists. 260 | ConvertLists, 261 | ParserEvent<'a> => ParserEvent<'a>, 262 | |this: &mut Self| { 263 | // TODO: Handle tight. 264 | 265 | // TODO: Allow changing the marker and number format. 266 | match this.iter.next() { 267 | // List start. 268 | Some(ParserEvent::Markdown(markdown::Event::Start(markdown::Tag::List(number)))) => { 269 | if let Some(start) = number { 270 | // Numbered list 271 | Some(ParserEvent::Typst(typst::Event::Start(typst::Tag::NumberedList(start, None, false)))) 272 | } else { 273 | // Bullet list 274 | Some(ParserEvent::Typst(typst::Event::Start(typst::Tag::BulletList(None, false)))) 275 | } 276 | 277 | }, 278 | // List end. 279 | Some(ParserEvent::Markdown(markdown::Event::End(markdown::Tag::List(number)))) => { 280 | if let Some(start) = number { 281 | // Numbered list 282 | Some(ParserEvent::Typst(typst::Event::End(typst::Tag::NumberedList(start, None, false)))) 283 | } else { 284 | // Bullet list 285 | Some(ParserEvent::Typst(typst::Event::End(typst::Tag::BulletList(None, false)))) 286 | } 287 | 288 | }, 289 | // List item start. 290 | Some(ParserEvent::Markdown(markdown::Event::Start(markdown::Tag::Item))) => { 291 | Some(ParserEvent::Typst(typst::Event::Start(typst::Tag::Item))) 292 | }, 293 | // List item end. 294 | Some(ParserEvent::Markdown(markdown::Event::End(markdown::Tag::Item))) => { 295 | Some(ParserEvent::Typst(typst::Event::End(typst::Tag::Item))) 296 | }, 297 | x => x, 298 | } 299 | } 300 | ); 301 | 302 | converter!( 303 | /// Convert Markdown headings to Typst headings. 304 | ConvertHeadings, 305 | ParserEvent<'a> => ParserEvent<'a>, 306 | |this: &mut Self| { 307 | struct TypstLevel(std::num::NonZeroU8); 308 | 309 | impl std::ops::Deref for TypstLevel { 310 | type Target = std::num::NonZeroU8; 311 | fn deref(&self) -> &Self::Target { 312 | &self.0 313 | } 314 | } 315 | impl From for TypstLevel{ 316 | fn from(item: markdown::HeadingLevel) -> Self { 317 | use markdown::HeadingLevel; 318 | match item { 319 | HeadingLevel::H1 => TypstLevel(core::num::NonZeroU8::new(1).expect("non-zero")), 320 | HeadingLevel::H2 => TypstLevel(core::num::NonZeroU8::new(2).expect("non-zero")), 321 | HeadingLevel::H3 => TypstLevel(core::num::NonZeroU8::new(3).expect("non-zero")), 322 | HeadingLevel::H4 => TypstLevel(core::num::NonZeroU8::new(4).expect("non-zero")), 323 | HeadingLevel::H5 => TypstLevel(core::num::NonZeroU8::new(5).expect("non-zero")), 324 | HeadingLevel::H6 => TypstLevel(core::num::NonZeroU8::new(6).expect("non-zero")), 325 | } 326 | } 327 | } 328 | match this.iter.next() { 329 | Some(ParserEvent::Markdown(markdown::Event::Start(markdown::Tag::Heading(level, _, _)))) => { 330 | let level: TypstLevel = level.into(); 331 | Some(ParserEvent::Typst(typst::Event::Start(typst::Tag::Heading(*level, 332 | typst::TableOfContents::Include, 333 | typst::Bookmarks::Include, 334 | )))) 335 | }, 336 | Some(ParserEvent::Markdown(markdown::Event::End(markdown::Tag::Heading(level, _, _)))) => { 337 | let level: TypstLevel = level.into(); 338 | Some(ParserEvent::Typst(typst::Event::End(typst::Tag::Heading(*level, 339 | typst::TableOfContents::Include, 340 | typst::Bookmarks::Include, 341 | )))) 342 | }, 343 | x => x, 344 | } 345 | } 346 | ); 347 | 348 | converter!( 349 | /// Convert Markdown tables to Typst tables. 350 | ConvertTables, 351 | ParserEvent<'a> => ParserEvent<'a>, 352 | |this: &mut Self| { 353 | match this.iter.next() { 354 | // Handle starting a table 355 | Some(ParserEvent::Markdown(markdown::Event::Start(markdown::Tag::Table(alignment)))) => { 356 | Some(ParserEvent::Typst(typst::Event::Start(typst::Tag::Table( 357 | alignment.iter().map(|&a| match a { 358 | markdown::Alignment::Left => typst::TableCellAlignment::Left, 359 | markdown::Alignment::Center => typst::TableCellAlignment::Center, 360 | markdown::Alignment::Right => typst::TableCellAlignment::Right, 361 | markdown::Alignment::None => typst::TableCellAlignment::None, 362 | }).collect(), 363 | )))) 364 | }, 365 | // Handle ending a table 366 | Some(ParserEvent::Markdown(markdown::Event::End(markdown::Tag::Table(alignment)))) => { 367 | Some(ParserEvent::Typst(typst::Event::End(typst::Tag::Table( 368 | alignment.iter().map(|&a| match a { 369 | markdown::Alignment::Left => typst::TableCellAlignment::Left, 370 | markdown::Alignment::Center => typst::TableCellAlignment::Center, 371 | markdown::Alignment::Right => typst::TableCellAlignment::Right, 372 | markdown::Alignment::None => typst::TableCellAlignment::None, 373 | }).collect(), 374 | )))) 375 | }, 376 | // Handle header row 377 | Some(ParserEvent::Markdown(markdown::Event::Start(markdown::Tag::TableHead))) => { 378 | Some(ParserEvent::Typst(typst::Event::Start(typst::Tag::TableHead))) 379 | }, 380 | Some(ParserEvent::Markdown(markdown::Event::End(markdown::Tag::TableHead))) => { 381 | Some(ParserEvent::Typst(typst::Event::End(typst::Tag::TableHead))) 382 | }, 383 | // Handle starting a row 384 | Some(ParserEvent::Markdown(markdown::Event::Start(markdown::Tag::TableRow))) => { 385 | Some(ParserEvent::Typst(typst::Event::Start(typst::Tag::TableRow))) 386 | }, 387 | // Handle ending a row 388 | Some(ParserEvent::Markdown(markdown::Event::End(markdown::Tag::TableRow))) => { 389 | Some(ParserEvent::Typst(typst::Event::End(typst::Tag::TableRow))) 390 | }, 391 | // Handle starting a cell 392 | Some(ParserEvent::Markdown(markdown::Event::Start(markdown::Tag::TableCell))) => { 393 | Some(ParserEvent::Typst(typst::Event::Start(typst::Tag::TableCell))) 394 | }, 395 | // Handle ending a cell 396 | Some(ParserEvent::Markdown(markdown::Event::End(markdown::Tag::TableCell))) => { 397 | Some(ParserEvent::Typst(typst::Event::End(typst::Tag::TableCell))) 398 | }, 399 | // Pass through any other events 400 | x => x, 401 | } 402 | } 403 | ); 404 | 405 | #[cfg(test)] 406 | mod tests { 407 | use super::*; 408 | use crate::markdown::CowStr; 409 | use crate::markdown::{MarkdownIter, Parser}; 410 | use similar_asserts::assert_eq; 411 | use std::num::NonZeroU8; 412 | 413 | // Set up type names so they are clearer and more succint. 414 | use markdown::Event as MdEvent; 415 | use markdown::HeadingLevel; 416 | use markdown::Tag as MdTag; 417 | use typst::Event as TypstEvent; 418 | use typst::Tag as TypstTag; 419 | use ParserEvent::*; 420 | 421 | /// Markdown docs: 422 | /// * https://spec.commonmark.org/0.30/#atx-headings 423 | /// * https://spec.commonmark.org/0.30/#setext-headings Typst docs: 424 | /// * https://typst.app/docs/reference/meta/heading/ 425 | mod headings { 426 | use super::*; 427 | 428 | #[test] 429 | fn convert_headings() { 430 | let md = "\ 431 | # Greetings 432 | 433 | ## This is **rad**! 434 | "; 435 | let i = ConvertHeadings::new(MarkdownIter(Parser::new(&md))); 436 | 437 | self::assert_eq!( 438 | i.collect::>(), 439 | vec![ 440 | Typst(TypstEvent::Start(TypstTag::Heading( 441 | NonZeroU8::new(1).unwrap(), 442 | typst::TableOfContents::Include, 443 | typst::Bookmarks::Include, 444 | ))), 445 | Markdown(MdEvent::Text(CowStr::Borrowed("Greetings"))), 446 | Typst(TypstEvent::End(TypstTag::Heading( 447 | NonZeroU8::new(1).unwrap(), 448 | typst::TableOfContents::Include, 449 | typst::Bookmarks::Include, 450 | ))), 451 | Typst(TypstEvent::Start(TypstTag::Heading( 452 | NonZeroU8::new(2).unwrap(), 453 | typst::TableOfContents::Include, 454 | typst::Bookmarks::Include, 455 | ))), 456 | Markdown(MdEvent::Text(CowStr::Borrowed("This is "))), 457 | Markdown(MdEvent::Start(MdTag::Strong)), 458 | Markdown(MdEvent::Text(CowStr::Borrowed("rad"))), 459 | Markdown(MdEvent::End(MdTag::Strong)), 460 | Markdown(MdEvent::Text(CowStr::Borrowed("!"))), 461 | Typst(TypstEvent::End(TypstTag::Heading( 462 | NonZeroU8::new(2).unwrap(), 463 | typst::TableOfContents::Include, 464 | typst::Bookmarks::Include, 465 | ))), 466 | ] 467 | ); 468 | } 469 | } 470 | 471 | /// Markdown docs: 472 | /// * https://spec.commonmark.org/0.30/#link-reference-definitions 473 | /// * https://spec.commonmark.org/0.30/#links 474 | /// * https://spec.commonmark.org/0.30/#autolinks Typst docs: 475 | /// * https://typst.app/docs/reference/meta/link/ 476 | mod links { 477 | use super::*; 478 | #[test] 479 | fn inline() { 480 | let md = "\ 481 | Cool [beans](https://example.com) 482 | "; 483 | let i = ConvertLinks::new(MarkdownIter(Parser::new(&md))); 484 | 485 | self::assert_eq!( 486 | i.collect::>(), 487 | vec![ 488 | Markdown(MdEvent::Start(MdTag::Paragraph)), 489 | Markdown(MdEvent::Text(CowStr::Borrowed("Cool "))), 490 | Typst(TypstEvent::Start(TypstTag::Link( 491 | typst::LinkType::Content, 492 | CowStr::Borrowed("https://example.com") 493 | ))), 494 | Markdown(MdEvent::Text(CowStr::Borrowed("beans"))), 495 | Typst(TypstEvent::End(TypstTag::Link( 496 | typst::LinkType::Content, 497 | CowStr::Borrowed("https://example.com") 498 | ))), 499 | Markdown(MdEvent::End(MdTag::Paragraph)), 500 | ] 501 | ); 502 | } 503 | 504 | #[test] 505 | fn auto() { 506 | let md = "\ 507 | Cool 508 | "; 509 | let i = ConvertLinks::new(MarkdownIter(Parser::new(&md))); 510 | 511 | self::assert_eq!( 512 | i.collect::>(), 513 | vec![ 514 | Markdown(MdEvent::Start(MdTag::Paragraph)), 515 | Markdown(MdEvent::Text(CowStr::Borrowed("Cool "))), 516 | Typst(TypstEvent::Start(TypstTag::Link( 517 | typst::LinkType::Autolink, 518 | CowStr::Borrowed("https://example.com") 519 | ))), 520 | Markdown(MdEvent::Text(CowStr::Borrowed("https://example.com"))), 521 | Typst(TypstEvent::End(TypstTag::Link( 522 | typst::LinkType::Autolink, 523 | CowStr::Borrowed("https://example.com") 524 | ))), 525 | Markdown(MdEvent::End(MdTag::Paragraph)), 526 | ] 527 | ); 528 | } 529 | 530 | #[test] 531 | fn email() { 532 | let md = "\ 533 | Who are 534 | "; 535 | let i = ConvertLinks::new(MarkdownIter(Parser::new(&md))); 536 | 537 | self::assert_eq!( 538 | i.collect::>(), 539 | vec![ 540 | Markdown(MdEvent::Start(MdTag::Paragraph)), 541 | Markdown(MdEvent::Text(CowStr::Borrowed("Who are "))), 542 | Typst(TypstEvent::Start(TypstTag::Link( 543 | typst::LinkType::Url, 544 | CowStr::Boxed("mailto:you@example.com".into()) 545 | ))), 546 | Markdown(MdEvent::Text(CowStr::Borrowed("you@example.com"))), 547 | Typst(TypstEvent::End(TypstTag::Link( 548 | typst::LinkType::Url, 549 | CowStr::Boxed("mailto:you@example.com".into()) 550 | ))), 551 | Markdown(MdEvent::End(MdTag::Paragraph)), 552 | ] 553 | ); 554 | } 555 | } 556 | 557 | /// Markdown docs: 558 | /// * https://spec.commonmark.org/0.30/#emphasis-and-strong-emphasis Typst docs: 559 | /// * https://typst.app/docs/reference/text/strong/ 560 | mod strong { 561 | use super::*; 562 | #[test] 563 | fn convert_strong() { 564 | let md = "\ 565 | ## **Foo** 566 | 567 | I **love** cake! 568 | "; 569 | let i = ConvertStrong::new(MarkdownIter(Parser::new(&md))); 570 | 571 | self::assert_eq!( 572 | i.collect::>(), 573 | vec![ 574 | Markdown(MdEvent::Start(MdTag::Heading( 575 | HeadingLevel::H2, 576 | None, 577 | vec![] 578 | ))), 579 | Typst(TypstEvent::Start(TypstTag::Strong)), 580 | Markdown(MdEvent::Text(CowStr::Borrowed("Foo"))), 581 | Typst(TypstEvent::End(TypstTag::Strong)), 582 | Markdown(MdEvent::End(MdTag::Heading(HeadingLevel::H2, None, vec![]))), 583 | Markdown(MdEvent::Start(MdTag::Paragraph)), 584 | Markdown(MdEvent::Text(CowStr::Borrowed("I "))), 585 | Typst(TypstEvent::Start(TypstTag::Strong)), 586 | Markdown(MdEvent::Text(CowStr::Borrowed("love"))), 587 | Typst(TypstEvent::End(TypstTag::Strong)), 588 | Markdown(MdEvent::Text(CowStr::Borrowed(" cake!"))), 589 | Markdown(MdEvent::End(MdTag::Paragraph)), 590 | ] 591 | ); 592 | } 593 | } 594 | 595 | /// Markdown docs: 596 | /// * https://spec.commonmark.org/0.30/#emphasis-and-strong-emphasis Typst docs: 597 | /// * https://typst.app/docs/reference/text/emph/ 598 | mod emphasis { 599 | use super::*; 600 | #[test] 601 | fn convert_emphasis() { 602 | let md = "\ 603 | ## _Foo_ 604 | 605 | I *love* cake! 606 | "; 607 | let i = ConvertEmphasis::new(MarkdownIter(Parser::new(&md))); 608 | 609 | self::assert_eq!( 610 | i.collect::>(), 611 | vec![ 612 | Markdown(MdEvent::Start(MdTag::Heading( 613 | HeadingLevel::H2, 614 | None, 615 | vec![] 616 | ))), 617 | Typst(TypstEvent::Start(TypstTag::Emphasis)), 618 | Markdown(MdEvent::Text(CowStr::Borrowed("Foo"))), 619 | Typst(TypstEvent::End(TypstTag::Emphasis)), 620 | Markdown(MdEvent::End(MdTag::Heading(HeadingLevel::H2, None, vec![]))), 621 | Markdown(MdEvent::Start(MdTag::Paragraph)), 622 | Markdown(MdEvent::Text(CowStr::Borrowed("I "))), 623 | Typst(TypstEvent::Start(TypstTag::Emphasis)), 624 | Markdown(MdEvent::Text(CowStr::Borrowed("love"))), 625 | Typst(TypstEvent::End(TypstTag::Emphasis)), 626 | Markdown(MdEvent::Text(CowStr::Borrowed(" cake!"))), 627 | Markdown(MdEvent::End(MdTag::Paragraph)), 628 | ] 629 | ); 630 | } 631 | } 632 | 633 | /// Markdown docs: 634 | /// * https://spec.commonmark.org/0.30/#code Typst docs: 635 | /// * https://typst.app/docs/reference/text/raw/ 636 | mod code { 637 | use super::*; 638 | #[test] 639 | fn inline() { 640 | let md = "\ 641 | foo `bar` baz 642 | "; 643 | let i = ConvertCode::new(MarkdownIter(Parser::new(&md))); 644 | 645 | self::assert_eq!( 646 | i.collect::>(), 647 | vec![ 648 | Markdown(MdEvent::Start(MdTag::Paragraph)), 649 | Markdown(MdEvent::Text(CowStr::Borrowed("foo "))), 650 | Typst(TypstEvent::Code(CowStr::Borrowed("bar"))), 651 | Markdown(MdEvent::Text(CowStr::Borrowed(" baz"))), 652 | Markdown(MdEvent::End(MdTag::Paragraph)), 653 | ] 654 | ); 655 | } 656 | 657 | #[test] 658 | fn block_indent() { 659 | let md = "\ 660 | whatever 661 | 662 | code 1 663 | code 2 664 | "; 665 | let i = ConvertCode::new(MarkdownIter(Parser::new(&md))); 666 | 667 | self::assert_eq!( 668 | i.collect::>(), 669 | vec![ 670 | Markdown(MdEvent::Start(MdTag::Paragraph)), 671 | Markdown(MdEvent::Text(CowStr::Borrowed("whatever"))), 672 | Markdown(MdEvent::End(MdTag::Paragraph)), 673 | Typst(TypstEvent::Start(TypstTag::CodeBlock( 674 | None, 675 | typst::CodeBlockDisplay::Block 676 | ))), 677 | Markdown(MdEvent::Text(CowStr::Borrowed("code 1\n"))), 678 | Markdown(MdEvent::Text(CowStr::Borrowed("code 2\n"))), 679 | Typst(TypstEvent::End(TypstTag::CodeBlock( 680 | None, 681 | typst::CodeBlockDisplay::Block 682 | ))), 683 | ] 684 | ); 685 | } 686 | 687 | #[test] 688 | fn block() { 689 | let md = "\ 690 | ``` 691 | blah 692 | ``` 693 | "; 694 | let i = ConvertCode::new(MarkdownIter(Parser::new(&md))); 695 | 696 | self::assert_eq!( 697 | i.collect::>(), 698 | vec![ 699 | Typst(TypstEvent::Start(TypstTag::CodeBlock( 700 | None, 701 | typst::CodeBlockDisplay::Block 702 | ))), 703 | Markdown(MdEvent::Text(CowStr::Borrowed("blah\n"))), 704 | Typst(TypstEvent::End(TypstTag::CodeBlock( 705 | None, 706 | typst::CodeBlockDisplay::Block 707 | ))), 708 | ] 709 | ); 710 | } 711 | 712 | #[test] 713 | fn block_with_fence() { 714 | let md = "\ 715 | ```foo 716 | blah 717 | ``` 718 | "; 719 | let i = ConvertCode::new(MarkdownIter(Parser::new(&md))); 720 | 721 | self::assert_eq!( 722 | i.collect::>(), 723 | vec![ 724 | Typst(TypstEvent::Start(TypstTag::CodeBlock( 725 | Some(CowStr::Borrowed("foo")), 726 | typst::CodeBlockDisplay::Block 727 | ))), 728 | Markdown(MdEvent::Text(CowStr::Borrowed("blah\n"))), 729 | Typst(TypstEvent::End(TypstTag::CodeBlock( 730 | Some(CowStr::Borrowed("foo")), 731 | typst::CodeBlockDisplay::Block 732 | ))), 733 | ] 734 | ); 735 | } 736 | } 737 | 738 | /// Markdown docs: 739 | /// * https://spec.commonmark.org/0.30/#text Typst docs: 740 | /// * https://typst.app/docs/reference/text/ 741 | mod text { 742 | use super::*; 743 | #[test] 744 | fn convert_text() { 745 | let md = "\ 746 | foo 747 | 748 | bar 749 | 750 | baz 751 | "; 752 | let i = ConvertText::new(MarkdownIter(Parser::new(&md))); 753 | 754 | self::assert_eq!( 755 | i.collect::>(), 756 | vec![ 757 | Markdown(MdEvent::Start(MdTag::Paragraph)), 758 | Typst(TypstEvent::Text(CowStr::Borrowed("foo"))), 759 | Markdown(MdEvent::End(MdTag::Paragraph)), 760 | Markdown(MdEvent::Start(MdTag::Paragraph)), 761 | Typst(TypstEvent::Text(CowStr::Borrowed("bar"))), 762 | Markdown(MdEvent::End(MdTag::Paragraph)), 763 | Markdown(MdEvent::Start(MdTag::Paragraph)), 764 | Typst(TypstEvent::Text(CowStr::Borrowed("baz"))), 765 | Markdown(MdEvent::End(MdTag::Paragraph)), 766 | ] 767 | ); 768 | } 769 | } 770 | 771 | /// Markdown docs: 772 | /// * https://spec.commonmark.org/0.31.2/#hard-line-breaks 773 | /// * https://spec.commonmark.org/0.31.2/#soft-line-breaks 774 | /// Typst docs: 775 | /// * https://typst.app/docs/reference/text/ 776 | mod breaks { 777 | use super::*; 778 | 779 | #[test] 780 | fn soft() { 781 | // Note that "foo" DOES NOT HAVE two spaces after it. 782 | let md = "\ 783 | foo 784 | bar 785 | "; 786 | let i = ConvertSoftBreaks::new(MarkdownIter(Parser::new(&md))); 787 | 788 | self::assert_eq!( 789 | i.collect::>(), 790 | vec![ 791 | Markdown(MdEvent::Start(MdTag::Paragraph)), 792 | Markdown(MdEvent::Text(CowStr::Borrowed("foo"))), 793 | Typst(TypstEvent::Text(CowStr::Borrowed(" "))), 794 | Markdown(MdEvent::Text(CowStr::Borrowed("bar"))), 795 | Markdown(MdEvent::End(MdTag::Paragraph)), 796 | ] 797 | ); 798 | } 799 | 800 | #[test] 801 | fn hard() { 802 | // Note that "foo" has two spaces after it. 803 | let md = "\ 804 | foo 805 | bar 806 | "; 807 | let i = ConvertHardBreaks::new(MarkdownIter(Parser::new(&md))); 808 | 809 | self::assert_eq!( 810 | i.collect::>(), 811 | vec![ 812 | Markdown(MdEvent::Start(MdTag::Paragraph)), 813 | Markdown(MdEvent::Text(CowStr::Borrowed("foo"))), 814 | Typst(TypstEvent::Linebreak), 815 | Markdown(MdEvent::Text(CowStr::Borrowed("bar"))), 816 | Markdown(MdEvent::End(MdTag::Paragraph)), 817 | ] 818 | ); 819 | } 820 | } 821 | 822 | /// Markdown docs: 823 | /// * https://spec.commonmark.org/0.30/#paragraphs Typst docs: 824 | /// * https://typst.app/docs/reference/layout/par/ 825 | mod paragraphs { 826 | use super::*; 827 | #[test] 828 | fn convert_paragraphs() { 829 | let md = "\ 830 | foo 831 | 832 | bar 833 | 834 | baz 835 | "; 836 | let i = ConvertParagraphs::new(MarkdownIter(Parser::new(&md))); 837 | 838 | self::assert_eq!( 839 | i.collect::>(), 840 | vec![ 841 | Typst(TypstEvent::Start(TypstTag::Paragraph)), 842 | Markdown(MdEvent::Text(CowStr::Borrowed("foo"))), 843 | Typst(TypstEvent::End(TypstTag::Paragraph)), 844 | Typst(TypstEvent::Start(TypstTag::Paragraph)), 845 | Markdown(MdEvent::Text(CowStr::Borrowed("bar"))), 846 | Typst(TypstEvent::End(TypstTag::Paragraph)), 847 | Typst(TypstEvent::Start(TypstTag::Paragraph)), 848 | Markdown(MdEvent::Text(CowStr::Borrowed("baz"))), 849 | Typst(TypstEvent::End(TypstTag::Paragraph)), 850 | ] 851 | ); 852 | } 853 | } 854 | 855 | /// Markdown docs: 856 | /// * https://spec.commonmark.org/0.30/#lists Typst docs: 857 | /// * https://typst.app/docs/reference/layout/list 858 | /// * https://typst.app/docs/reference/layout/enum/ 859 | mod lists { 860 | use super::*; 861 | 862 | #[test] 863 | fn bullet() { 864 | let md = "\ 865 | * dogs 866 | * are 867 | * cool 868 | "; 869 | let i = ConvertLists::new(MarkdownIter(Parser::new(&md))); 870 | 871 | self::assert_eq!( 872 | i.collect::>(), 873 | vec![ 874 | Typst(TypstEvent::Start(TypstTag::BulletList(None, false))), 875 | // First bulet. 876 | Typst(TypstEvent::Start(TypstTag::Item)), 877 | Markdown(MdEvent::Text(CowStr::Borrowed("dogs"))), 878 | Typst(TypstEvent::End(TypstTag::Item)), 879 | // Second bullet. 880 | Typst(TypstEvent::Start(TypstTag::Item)), 881 | Markdown(MdEvent::Text(CowStr::Borrowed("are"))), 882 | Typst(TypstEvent::End(TypstTag::Item)), 883 | // Third bullet. 884 | Typst(TypstEvent::Start(TypstTag::Item)), 885 | Markdown(MdEvent::Text(CowStr::Borrowed("cool"))), 886 | Typst(TypstEvent::End(TypstTag::Item)), 887 | Typst(TypstEvent::End(TypstTag::BulletList(None, false))), 888 | ], 889 | ); 890 | } 891 | 892 | #[test] 893 | fn numbered() { 894 | let md = "\ 895 | 1. cats are _too_ 896 | 2. birds are ok 897 | "; 898 | let i = ConvertLists::new(MarkdownIter(Parser::new(&md))); 899 | 900 | self::assert_eq!( 901 | i.collect::>(), 902 | vec![ 903 | Typst(TypstEvent::Start(TypstTag::NumberedList(1, None, false))), 904 | // First bullet 905 | Typst(TypstEvent::Start(TypstTag::Item)), 906 | Markdown(MdEvent::Text(CowStr::Borrowed("cats are "))), 907 | Markdown(MdEvent::Start(MdTag::Emphasis)), 908 | Markdown(MdEvent::Text(CowStr::Borrowed("too"))), 909 | Markdown(MdEvent::End(MdTag::Emphasis)), 910 | Typst(TypstEvent::End(TypstTag::Item)), 911 | // Second bullet 912 | Typst(TypstEvent::Start(TypstTag::Item)), 913 | Markdown(MdEvent::Text(CowStr::Borrowed("birds are ok"))), 914 | Typst(TypstEvent::End(TypstTag::Item)), 915 | Typst(TypstEvent::End(TypstTag::NumberedList(1, None, false))), 916 | ], 917 | ); 918 | } 919 | 920 | #[test] 921 | fn numbered_custom_start() { 922 | let md = "\ 923 | 6. foo 924 | 1. bar 925 | "; 926 | let i = ConvertLists::new(MarkdownIter(Parser::new(&md))); 927 | 928 | self::assert_eq!( 929 | i.collect::>(), 930 | vec![ 931 | Typst(TypstEvent::Start(TypstTag::NumberedList(6, None, false))), 932 | // First bullet. 933 | Typst(TypstEvent::Start(TypstTag::Item)), 934 | Markdown(MdEvent::Text(CowStr::Borrowed("foo"))), 935 | Typst(TypstEvent::End(TypstTag::Item)), 936 | // Second bullet. 937 | Typst(TypstEvent::Start(TypstTag::Item)), 938 | Markdown(MdEvent::Text(CowStr::Borrowed("bar"))), 939 | Typst(TypstEvent::End(TypstTag::Item)), 940 | Typst(TypstEvent::End(TypstTag::NumberedList(6, None, false))), 941 | ], 942 | ); 943 | } 944 | 945 | #[test] 946 | fn multiple_lines() { 947 | let md = "\ 948 | * multiple 949 | lines 950 | "; 951 | let i = ConvertLists::new(MarkdownIter(Parser::new(&md))); 952 | 953 | self::assert_eq!( 954 | i.collect::>(), 955 | vec![ 956 | Typst(TypstEvent::Start(TypstTag::BulletList(None, false))), 957 | // First bullet. 958 | Typst(TypstEvent::Start(TypstTag::Item)), 959 | Markdown(MdEvent::Text(CowStr::Borrowed("multiple"))), 960 | Markdown(MdEvent::SoftBreak), 961 | Markdown(MdEvent::Text(CowStr::Borrowed("lines"))), 962 | Typst(TypstEvent::End(TypstTag::Item)), 963 | Typst(TypstEvent::End(TypstTag::BulletList(None, false))), 964 | ] 965 | ); 966 | } 967 | } 968 | 969 | mod issues { 970 | use super::*; 971 | 972 | // https://github.com/LegNeato/mdbook-typst/issues/3 973 | #[test] 974 | fn backslashes_in_backticks() { 975 | let md = r###"before `\` after"###; 976 | 977 | let i = ConvertText::new(MarkdownIter(Parser::new(&md))); 978 | 979 | self::assert_eq!( 980 | i.collect::>(), 981 | vec![ 982 | Markdown(MdEvent::Start(MdTag::Paragraph)), 983 | Typst(TypstEvent::Text("before ".into())), 984 | Markdown(MdEvent::Code(r#"\"#.into())), 985 | Typst(TypstEvent::Text(" after".into())), 986 | Markdown(MdEvent::End(MdTag::Paragraph)), 987 | ], 988 | ); 989 | } 990 | 991 | // https://github.com/LegNeato/mdbook-typst/issues/9 992 | #[test] 993 | fn simple_blockquote() { 994 | let md = "> test"; 995 | 996 | let i = ConvertBlockQuotes::new(MarkdownIter(Parser::new(&md))); 997 | 998 | self::assert_eq!( 999 | i.collect::>(), 1000 | vec![ 1001 | Typst(TypstEvent::Start(TypstTag::Quote( 1002 | typst::QuoteType::Block, 1003 | typst::QuoteQuotes::Auto, 1004 | None, 1005 | ))), 1006 | Markdown(MdEvent::Start(MdTag::Paragraph)), 1007 | Markdown(MdEvent::Text(CowStr::Borrowed("test"))), 1008 | Markdown(MdEvent::End(MdTag::Paragraph)), 1009 | Typst(TypstEvent::End(TypstTag::Quote( 1010 | typst::QuoteType::Block, 1011 | typst::QuoteQuotes::Auto, 1012 | None, 1013 | ))), 1014 | ], 1015 | ); 1016 | } 1017 | 1018 | // https://github.com/LegNeato/mdbook-typst/issues/9 1019 | #[test] 1020 | fn complex_blockquote() { 1021 | let md = "> one\n> two\n> three"; 1022 | 1023 | let i = ConvertBlockQuotes::new(MarkdownIter(Parser::new(&md))); 1024 | 1025 | self::assert_eq!( 1026 | i.collect::>(), 1027 | vec![ 1028 | Typst(TypstEvent::Start(TypstTag::Quote( 1029 | typst::QuoteType::Block, 1030 | typst::QuoteQuotes::Auto, 1031 | None, 1032 | ))), 1033 | Markdown(MdEvent::Start(MdTag::Paragraph)), 1034 | Markdown(MdEvent::Text(CowStr::Borrowed("one"))), 1035 | Markdown(MdEvent::SoftBreak), 1036 | Markdown(MdEvent::Text(CowStr::Borrowed("two"))), 1037 | Markdown(MdEvent::SoftBreak), 1038 | Markdown(MdEvent::Text(CowStr::Borrowed("three"))), 1039 | Markdown(MdEvent::End(MdTag::Paragraph)), 1040 | Typst(TypstEvent::End(TypstTag::Quote( 1041 | typst::QuoteType::Block, 1042 | typst::QuoteQuotes::Auto, 1043 | None, 1044 | ))), 1045 | ], 1046 | ); 1047 | } 1048 | } 1049 | 1050 | mod tables { 1051 | use super::*; 1052 | 1053 | #[test] 1054 | fn simple_table() { 1055 | let md = "\ 1056 | | Header1 | Header2 | 1057 | |---------|---------| 1058 | | Cell1 | Cell2 | 1059 | "; 1060 | let i = ConvertTables::new(MarkdownIter(Parser::new_ext( 1061 | &md, 1062 | pulldown_cmark::Options::ENABLE_TABLES, 1063 | ))); 1064 | 1065 | self::assert_eq!( 1066 | i.collect::>(), 1067 | vec![ 1068 | Typst(TypstEvent::Start(TypstTag::Table(vec![ 1069 | typst::TableCellAlignment::None, 1070 | typst::TableCellAlignment::None, 1071 | ]))), 1072 | Typst(TypstEvent::Start(TypstTag::TableHead)), 1073 | Typst(TypstEvent::Start(TypstTag::TableCell)), 1074 | Markdown(MdEvent::Text(CowStr::Borrowed("Header1"))), 1075 | Typst(TypstEvent::End(TypstTag::TableCell)), 1076 | Typst(TypstEvent::Start(TypstTag::TableCell)), 1077 | Markdown(MdEvent::Text(CowStr::Borrowed("Header2"))), 1078 | Typst(TypstEvent::End(TypstTag::TableCell)), 1079 | Typst(TypstEvent::End(TypstTag::TableHead)), 1080 | Typst(TypstEvent::Start(TypstTag::TableRow)), 1081 | Typst(TypstEvent::Start(TypstTag::TableCell)), 1082 | Markdown(MdEvent::Text(CowStr::Borrowed("Cell1"))), 1083 | Typst(TypstEvent::End(TypstTag::TableCell)), 1084 | Typst(TypstEvent::Start(TypstTag::TableCell)), 1085 | Markdown(MdEvent::Text(CowStr::Borrowed("Cell2"))), 1086 | Typst(TypstEvent::End(TypstTag::TableCell)), 1087 | Typst(TypstEvent::End(TypstTag::TableRow)), 1088 | Typst(TypstEvent::End(TypstTag::Table(vec![ 1089 | typst::TableCellAlignment::None, 1090 | typst::TableCellAlignment::None, 1091 | ]))), 1092 | ] 1093 | ); 1094 | } 1095 | 1096 | #[test] 1097 | fn table_with_alignment() { 1098 | let md = "\ 1099 | | Header1 | Header2 | 1100 | |:--------|:-------:| 1101 | | Cell1 | Cell2 | 1102 | "; 1103 | let i = ConvertTables::new(MarkdownIter(Parser::new_ext( 1104 | &md, 1105 | pulldown_cmark::Options::ENABLE_TABLES, 1106 | ))); 1107 | 1108 | self::assert_eq!( 1109 | i.collect::>(), 1110 | vec![ 1111 | Typst(TypstEvent::Start(TypstTag::Table(vec![ 1112 | typst::TableCellAlignment::Left, 1113 | typst::TableCellAlignment::Center, 1114 | ]))), 1115 | Typst(TypstEvent::Start(TypstTag::TableHead)), 1116 | Typst(TypstEvent::Start(TypstTag::TableCell)), 1117 | Markdown(MdEvent::Text(CowStr::Borrowed("Header1"))), 1118 | Typst(TypstEvent::End(TypstTag::TableCell)), 1119 | Typst(TypstEvent::Start(TypstTag::TableCell)), 1120 | Markdown(MdEvent::Text(CowStr::Borrowed("Header2"))), 1121 | Typst(TypstEvent::End(TypstTag::TableCell)), 1122 | Typst(TypstEvent::End(TypstTag::TableHead)), 1123 | Typst(TypstEvent::Start(TypstTag::TableRow)), 1124 | Typst(TypstEvent::Start(TypstTag::TableCell)), 1125 | Markdown(MdEvent::Text(CowStr::Borrowed("Cell1"))), 1126 | Typst(TypstEvent::End(TypstTag::TableCell)), 1127 | Typst(TypstEvent::Start(TypstTag::TableCell)), 1128 | Markdown(MdEvent::Text(CowStr::Borrowed("Cell2"))), 1129 | Typst(TypstEvent::End(TypstTag::TableCell)), 1130 | Typst(TypstEvent::End(TypstTag::TableRow)), 1131 | Typst(TypstEvent::End(TypstTag::Table(vec![ 1132 | typst::TableCellAlignment::Left, 1133 | typst::TableCellAlignment::Center, 1134 | ]))), 1135 | ] 1136 | ); 1137 | } 1138 | } 1139 | } 1140 | -------------------------------------------------------------------------------- /pullup/src/mdbook/mod.rs: -------------------------------------------------------------------------------- 1 | //! Support for [mdBook](https://github.com/rust-lang/mdBook). 2 | 3 | use crate::ParserEvent; 4 | pub use pulldown_mdbook::{ChapterSource, ChapterStatus, ContentType, Event, Parser, Tag}; 5 | 6 | pub mod to; 7 | 8 | /// Assert that an iterator only contains mdBook events. Panics if another type of event 9 | /// is encountered. 10 | /// 11 | /// For a non-panic version, see [`MdbookFilter`]. 12 | pub struct AssertMdbook(pub T); 13 | impl<'a, T> Iterator for AssertMdbook 14 | where 15 | T: Iterator>, 16 | { 17 | type Item = self::Event<'a>; 18 | 19 | fn next(&mut self) -> Option { 20 | match self.0.next() { 21 | None => None, 22 | Some(ParserEvent::Mdbook(x)) => Some(x), 23 | #[cfg(feature = "markdown")] 24 | Some(ParserEvent::Markdown(x)) => panic!("unexpected markdown event: {x:?}"), 25 | #[cfg(feature = "typst")] 26 | Some(ParserEvent::Typst(x)) => panic!("unexpected typst event: {x:?}"), 27 | } 28 | } 29 | } 30 | 31 | /// An iterator that only contains mdBook events. Other types of events will be filtered 32 | /// out. 33 | /// 34 | /// To panic when a non-mdBook event is encountered, see [`AssertMdbook`]. 35 | pub struct MdbookFilter(pub T); 36 | impl<'a, T> Iterator for MdbookFilter 37 | where 38 | T: Iterator>, 39 | { 40 | type Item = Event<'a>; 41 | 42 | fn next(&mut self) -> Option { 43 | match self.0.next() { 44 | None => None, 45 | Some(ParserEvent::Mdbook(x)) => Some(x), 46 | #[cfg(feature = "markdown")] 47 | Some(ParserEvent::Markdown(_)) => self.next(), 48 | #[cfg(feature = "typst")] 49 | Some(ParserEvent::Typst(_)) => self.next(), 50 | } 51 | } 52 | } 53 | 54 | /// An adaptor for events from an mdBook parser. 55 | pub struct MdbookIter(pub T); 56 | 57 | impl<'a, T> Iterator for MdbookIter 58 | where 59 | T: Iterator>, 60 | { 61 | type Item = ParserEvent<'a>; 62 | 63 | fn next(&mut self) -> Option { 64 | self.0.next().map(ParserEvent::Mdbook) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /pullup/src/mdbook/to/mod.rs: -------------------------------------------------------------------------------- 1 | //! Convert mdBook _to_ other formats. 2 | 3 | #[cfg(feature = "typst")] 4 | pub mod typst; 5 | -------------------------------------------------------------------------------- /pullup/src/mdbook/to/typst/builder.rs: -------------------------------------------------------------------------------- 1 | //! Builder to customize mdBook to Typst conversion. 2 | 3 | use core::marker::PhantomData; 4 | 5 | use crate::markdown::to::typst::*; 6 | use crate::mdbook::to::typst::*; 7 | use crate::mdbook::MdbookIter; 8 | use crate::ParserEvent; 9 | 10 | #[derive(typed_builder::TypedBuilder)] 11 | #[builder(build_method(vis="", name=__build))] 12 | #[builder(field_defaults(default = true))] 13 | /// Converts Mdbook to Typst. 14 | /// 15 | /// Using the builder one can choose which conversions to apply. By default, all 16 | /// conversions are enabled. 17 | /// 18 | /// For more control over conversion, use the converters in [the parent 19 | /// module](crate::mdbook::to::typst) and [markdown module](crate::markdown::to::typst) 20 | /// directly. Additionally, one may turn off the conversion via the builder and operate 21 | /// on the resulting [`ParseEvent`](crate::ParserEvent) iterator. 22 | pub struct Conversion<'a, T> { 23 | #[builder(!default)] 24 | events: T, 25 | title: bool, 26 | authors: bool, 27 | chapters: bool, 28 | content: bool, 29 | headings: bool, 30 | paragraphs: bool, 31 | soft_breaks: bool, 32 | hard_breaks: bool, 33 | text: bool, 34 | strong: bool, 35 | emphasis: bool, 36 | blockquotes: bool, 37 | lists: bool, 38 | code: bool, 39 | links: bool, 40 | tables: bool, 41 | #[builder(default)] 42 | _p: PhantomData<&'a ()>, 43 | } 44 | 45 | #[allow(dead_code, non_camel_case_types, missing_docs)] 46 | #[automatically_derived] 47 | impl< 48 | 'a, 49 | T, 50 | __title: ::typed_builder::Optional, 51 | __authors: ::typed_builder::Optional, 52 | __chapters: ::typed_builder::Optional, 53 | __content: ::typed_builder::Optional, 54 | __headings: ::typed_builder::Optional, 55 | __paragraphs: ::typed_builder::Optional, 56 | __soft_breaks: ::typed_builder::Optional, 57 | __hard_breaks: ::typed_builder::Optional, 58 | __text: ::typed_builder::Optional, 59 | __strong: ::typed_builder::Optional, 60 | __emphasis: ::typed_builder::Optional, 61 | __blockquotes: ::typed_builder::Optional, 62 | __lists: ::typed_builder::Optional, 63 | __code: ::typed_builder::Optional, 64 | __links: ::typed_builder::Optional, 65 | __tables: ::typed_builder::Optional, 66 | ___p: ::typed_builder::Optional>, 67 | > 68 | ConversionBuilder< 69 | 'a, 70 | T, 71 | ( 72 | (T,), 73 | __title, 74 | __authors, 75 | __chapters, 76 | __content, 77 | __headings, 78 | __paragraphs, 79 | __soft_breaks, 80 | __hard_breaks, 81 | __text, 82 | __strong, 83 | __emphasis, 84 | __blockquotes, 85 | __lists, 86 | __code, 87 | __links, 88 | __tables, 89 | ___p, 90 | ), 91 | > 92 | where 93 | T: Iterator> + 'a, 94 | { 95 | pub fn build(self) -> impl Iterator> { 96 | let this = self.__build(); 97 | let mut events: Box>> = 98 | Box::new(MdbookIter(this.events)); 99 | if this.title { 100 | events = Box::new(ConvertTitle::new(events)); 101 | } 102 | if this.authors { 103 | events = Box::new(ConvertAuthors::new(events)); 104 | } 105 | if this.chapters { 106 | events = Box::new(ConvertChapter::new(events)); 107 | } 108 | if this.content { 109 | events = Box::new(events.map(|e| match e { 110 | ParserEvent::Mdbook(mdbook::Event::MarkdownContentEvent(m)) => { 111 | ParserEvent::Markdown(m) 112 | } 113 | x => x, 114 | })); 115 | if this.headings { 116 | events = Box::new(ConvertHeadings::new(events)); 117 | } 118 | if this.paragraphs { 119 | events = Box::new(ConvertParagraphs::new(events)); 120 | } 121 | if this.soft_breaks { 122 | events = Box::new(ConvertSoftBreaks::new(events)); 123 | } 124 | if this.hard_breaks { 125 | events = Box::new(ConvertHardBreaks::new(events)); 126 | } 127 | if this.text { 128 | events = Box::new(ConvertText::new(events)); 129 | } 130 | if this.strong { 131 | events = Box::new(ConvertStrong::new(events)); 132 | } 133 | if this.emphasis { 134 | events = Box::new(ConvertEmphasis::new(events)); 135 | } 136 | if this.blockquotes { 137 | events = Box::new(ConvertBlockQuotes::new(events)); 138 | } 139 | if this.lists { 140 | events = Box::new(ConvertLists::new(events)); 141 | } 142 | if this.code { 143 | events = Box::new(ConvertCode::new(events)); 144 | } 145 | if this.links { 146 | events = Box::new(ConvertLinks::new(events)); 147 | } 148 | if this.tables { 149 | events = Box::new(ConvertTables::new(events)); 150 | } 151 | } 152 | 153 | events 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /pullup/src/mdbook/to/typst/mod.rs: -------------------------------------------------------------------------------- 1 | //! Convert mdBook to Typst. 2 | 3 | use crate::converter; 4 | use crate::markdown; 5 | use crate::markdown::CowStr; 6 | use crate::mdbook; 7 | use crate::typst; 8 | use crate::ParserEvent; 9 | 10 | use core::cmp::min; 11 | use core::num::NonZeroU8; 12 | use std::collections::VecDeque; 13 | 14 | #[cfg(feature = "builder")] 15 | mod builder; 16 | 17 | #[cfg(feature = "builder")] 18 | pub use builder::Conversion; 19 | 20 | /// Convert mdBook authors to Typst authors. 21 | #[derive(Debug)] 22 | pub struct ConvertAuthors<'a, T> { 23 | authors: Vec>, 24 | iter: T, 25 | } 26 | 27 | impl<'a, T> ConvertAuthors<'a, T> 28 | where 29 | T: Iterator>, 30 | { 31 | #[allow(dead_code)] 32 | fn new(iter: T) -> Self { 33 | Self { 34 | authors: vec![], 35 | iter, 36 | } 37 | } 38 | } 39 | 40 | impl<'a, T> Iterator for ConvertAuthors<'a, T> 41 | where 42 | T: Iterator>, 43 | { 44 | type Item = ParserEvent<'a>; 45 | 46 | fn next(&mut self) -> Option { 47 | match self.iter.next() { 48 | Some(ParserEvent::Mdbook(mdbook::Event::Start(mdbook::Tag::AuthorList))) => { 49 | self.authors = vec![]; 50 | self.next() 51 | } 52 | Some(ParserEvent::Mdbook(mdbook::Event::Author(a))) => { 53 | self.authors.push(a); 54 | self.next() 55 | } 56 | Some(ParserEvent::Mdbook(mdbook::Event::End(mdbook::Tag::AuthorList))) => { 57 | if !self.authors.is_empty() { 58 | let markup_array = format!( 59 | "({})", 60 | // TODO: use intersperse once stable. 61 | self.authors 62 | .iter() 63 | .map(|x| format!("\"{}\"", x)) 64 | .collect::>() 65 | .join(",") 66 | ); 67 | return Some(ParserEvent::Typst(typst::Event::DocumentSet( 68 | "author".into(), 69 | markup_array.into(), 70 | ))); 71 | } 72 | self.next() 73 | } 74 | x => x, 75 | } 76 | } 77 | } 78 | 79 | // TODO: tests 80 | converter!( 81 | /// Convert mdBook title to Typst set document title event. 82 | ConvertTitle, 83 | ParserEvent<'a> => ParserEvent<'a>, 84 | |this: &mut Self| { 85 | match this.iter.next() { 86 | Some(ParserEvent::Mdbook(mdbook::Event::Title(title))) => { 87 | Some(ParserEvent::Typst(typst::Event::DocumentSet( 88 | "title".into(), 89 | format!("\"{}\"", title.as_ref()).into()), 90 | )) 91 | }, 92 | x => x, 93 | } 94 | }); 95 | 96 | #[derive(Debug)] 97 | pub struct ConvertChapter<'a, T> { 98 | chapters: VecDeque<()>, 99 | buf: VecDeque>, 100 | iter: T, 101 | } 102 | 103 | impl<'a, T> ConvertChapter<'a, T> 104 | where 105 | T: Iterator>, 106 | { 107 | #[allow(dead_code)] 108 | fn new(iter: T) -> Self { 109 | Self { 110 | chapters: VecDeque::new(), 111 | buf: VecDeque::new(), 112 | iter, 113 | } 114 | } 115 | } 116 | 117 | impl<'a, T> Iterator for ConvertChapter<'a, T> 118 | where 119 | T: Iterator>, 120 | { 121 | type Item = ParserEvent<'a>; 122 | 123 | fn next(&mut self) -> Option { 124 | // If we have a previously buffered event, return it. 125 | if let Some(buffered) = self.buf.pop_front() { 126 | #[cfg(feature = "tracing")] 127 | tracing::trace!("returning buffered: {:?}", buffered); 128 | 129 | return Some(buffered); 130 | } 131 | // Otherwise pull from the inner iterator. 132 | match self.iter.next() { 133 | // Start of chapter. 134 | Some(ParserEvent::Mdbook(mdbook::Event::Start(mdbook::Tag::Chapter( 135 | _, 136 | name, 137 | _, 138 | _, 139 | )))) => { 140 | #[cfg(feature = "tracing")] 141 | tracing::trace!("chapter start: {}", name); 142 | 143 | // Get how many chapters deep we are. 144 | let depth = self.chapters.len(); 145 | 146 | // Create a Typst heading start event for the chapter. 147 | let tag = typst::Tag::Heading( 148 | NonZeroU8::new((1 + depth).try_into().expect("nonzero")).expect("nonzero"), 149 | typst::TableOfContents::Include, 150 | typst::Bookmarks::Include, 151 | ); 152 | 153 | let start_event = ParserEvent::Typst(typst::Event::Start(tag.clone())); 154 | let end_event = ParserEvent::Typst(typst::Event::End(tag)); 155 | 156 | // Queue up the chapter name text event and heading end event. 157 | self.buf 158 | .push_back(ParserEvent::Typst(typst::Event::Text(name.clone()))); 159 | self.buf.push_back(end_event); 160 | 161 | // Record that we are one chapter deeper. 162 | self.chapters.push_back(()); 163 | 164 | #[cfg(feature = "tracing")] 165 | tracing::trace!("returning: {:?}", start_event); 166 | 167 | // Return the heading start event. 168 | Some(start_event) 169 | } 170 | // End of a chapter. 171 | Some(ParserEvent::Mdbook(mdbook::Event::End(mdbook::Tag::Chapter(_, _name, _, _)))) => { 172 | #[cfg(feature = "tracing")] 173 | tracing::trace!("chapter end: {}", _name); 174 | 175 | // Record that we are one chapter shallower. 176 | let _ = self.chapters.pop_front(); 177 | // Chapters are converted to page break. 178 | Some(ParserEvent::Typst(typst::Event::FunctionCall( 179 | None, 180 | "pagebreak".into(), 181 | vec!["weak: true".into()], 182 | ))) 183 | } 184 | // Heading start in a chapter. 185 | Some(ParserEvent::Mdbook(mdbook::Event::MarkdownContentEvent( 186 | markdown::Event::Start(markdown::Tag::Heading(level, x, y)), 187 | ))) => { 188 | #[cfg(feature = "tracing")] 189 | tracing::trace!("heading start: {}", level); 190 | 191 | use markdown::HeadingLevel; 192 | // Get how many chapters deep we are. 193 | let depth = self.chapters.len(); 194 | let new_level = match level { 195 | HeadingLevel::H1 => { 196 | HeadingLevel::try_from(min(1 + depth, 6)).expect("valid heading level") 197 | } 198 | HeadingLevel::H2 => { 199 | HeadingLevel::try_from(min(2 + depth, 6)).expect("valid heading level") 200 | } 201 | HeadingLevel::H3 => { 202 | HeadingLevel::try_from(min(3 + depth, 6)).expect("valid heading level") 203 | } 204 | HeadingLevel::H4 => { 205 | HeadingLevel::try_from(min(4 + depth, 6)).expect("valid heading level") 206 | } 207 | HeadingLevel::H5 => { 208 | HeadingLevel::try_from(min(5 + depth, 6)).expect("valid heading level") 209 | } 210 | HeadingLevel::H6 => HeadingLevel::H6, 211 | }; 212 | self.buf 213 | .push_back(ParserEvent::Mdbook(mdbook::Event::MarkdownContentEvent( 214 | markdown::Event::Start(markdown::Tag::Heading(new_level, x, y)), 215 | ))); 216 | self.next() 217 | } 218 | // Heading end in a chapter. 219 | Some(ParserEvent::Mdbook(mdbook::Event::MarkdownContentEvent( 220 | markdown::Event::End(markdown::Tag::Heading(level, x, y)), 221 | ))) => { 222 | #[cfg(feature = "tracing")] 223 | tracing::trace!("heading end: {}", level); 224 | use markdown::HeadingLevel; 225 | // Get how many chapters deep we are. 226 | let depth = self.chapters.len(); 227 | let new_level = match level { 228 | HeadingLevel::H1 => { 229 | HeadingLevel::try_from(min(1 + depth, 6)).expect("valid heading level") 230 | } 231 | HeadingLevel::H2 => { 232 | HeadingLevel::try_from(min(2 + depth, 6)).expect("valid heading level") 233 | } 234 | HeadingLevel::H3 => { 235 | HeadingLevel::try_from(min(3 + depth, 6)).expect("valid heading level") 236 | } 237 | HeadingLevel::H4 => { 238 | HeadingLevel::try_from(min(4 + depth, 6)).expect("valid heading level") 239 | } 240 | HeadingLevel::H5 => { 241 | HeadingLevel::try_from(min(5 + depth, 6)).expect("valid heading level") 242 | } 243 | HeadingLevel::H6 => HeadingLevel::H6, 244 | }; 245 | self.buf 246 | .push_back(ParserEvent::Mdbook(mdbook::Event::MarkdownContentEvent( 247 | markdown::Event::End(markdown::Tag::Heading(new_level, x, y)), 248 | ))); 249 | self.next() 250 | } 251 | 252 | x => x, 253 | } 254 | } 255 | } 256 | 257 | // TODO: tests 258 | converter!( 259 | /// Convert mdBook chapters to Typst pagebreaks. This does not affect any content in 260 | /// the chapter. 261 | ConvertChapterToPagebreak, 262 | ParserEvent<'a> => ParserEvent<'a>, 263 | |this: &mut Self| { 264 | match this.iter.next() { 265 | Some(ParserEvent::Mdbook(mdbook::Event::Start(mdbook::Tag::Chapter(_, _, _, _)))) => this.next(), 266 | Some(ParserEvent::Mdbook(mdbook::Event::End(mdbook::Tag::Chapter(_, _, _, _)))) => { 267 | Some(ParserEvent::Typst(typst::Event::FunctionCall( 268 | None, 269 | "pagebreak".into(), 270 | vec!["weak: true".into()], 271 | ))) 272 | }, 273 | x => x, 274 | } 275 | }); 276 | -------------------------------------------------------------------------------- /pullup/src/typst/mod.rs: -------------------------------------------------------------------------------- 1 | //! Support for [Typist](https://typst.app/docs). 2 | 3 | pub use pulldown_typst::{ 4 | Bookmarks, CodeBlockDisplay, Event, LinkType, NumberingPattern, QuoteQuotes, QuoteType, 5 | ShowType, TableCellAlignment, TableOfContents, Tag, 6 | }; 7 | 8 | use crate::ParserEvent; 9 | 10 | pub mod to; 11 | 12 | /// Assert that an iterator only contains Typst events. Panics if another type of event 13 | /// is encountered. 14 | /// 15 | /// For a non-panic version, see [`TypstFilter`]. 16 | pub struct AssertTypst(pub T); 17 | impl<'a, T> Iterator for AssertTypst 18 | where 19 | T: Iterator>, 20 | { 21 | type Item = self::Event<'a>; 22 | 23 | fn next(&mut self) -> Option { 24 | match self.0.next() { 25 | None => None, 26 | Some(ParserEvent::Typst(x)) => Some(x), 27 | #[cfg(feature = "markdown")] 28 | Some(ParserEvent::Markdown(x)) => panic!("unexpected markdown event: {x:?}"), 29 | #[cfg(feature = "mdbook")] 30 | Some(ParserEvent::Mdbook(x)) => panic!("unexpected mdbook event: {x:?}"), 31 | } 32 | } 33 | } 34 | 35 | /// An iterator that only contains Typst events. Other types of events will be filtered 36 | /// out. 37 | /// 38 | /// To panic when a non-Typst event is encountered, see [`AssertTypst`]. 39 | pub struct TypstFilter(pub T); 40 | impl<'a, T> Iterator for TypstFilter 41 | where 42 | T: Iterator>, 43 | { 44 | type Item = Event<'a>; 45 | 46 | fn next(&mut self) -> Option { 47 | match self.0.next() { 48 | None => None, 49 | Some(ParserEvent::Typst(x)) => Some(x), 50 | #[cfg(feature = "markdown")] 51 | Some(ParserEvent::Markdown(_)) => self.next(), 52 | #[cfg(feature = "mdbook")] 53 | Some(ParserEvent::Mdbook(_)) => self.next(), 54 | } 55 | } 56 | } 57 | 58 | /// An adaptor for events from a Typst parser. 59 | pub struct TypstIter(pub T); 60 | 61 | impl<'a, T> Iterator for TypstIter 62 | where 63 | T: Iterator>, 64 | { 65 | type Item = ParserEvent<'a>; 66 | 67 | fn next(&mut self) -> Option { 68 | self.0.next().map(ParserEvent::Typst) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /pullup/src/typst/to/markup.rs: -------------------------------------------------------------------------------- 1 | //! Adaptors for converting from Typst events to Typst markup. 2 | 3 | pub use pulldown_typst::markup::*; 4 | -------------------------------------------------------------------------------- /pullup/src/typst/to/mod.rs: -------------------------------------------------------------------------------- 1 | //! Convert Typst _to_ other formats. 2 | 3 | pub mod markup; 4 | --------------------------------------------------------------------------------