├── .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 |
--------------------------------------------------------------------------------