├── .github └── workflows │ └── rust.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE-APACHE ├── Makefile ├── README.md ├── examples └── stupicat.rs ├── rustfmt.toml ├── src ├── lib.rs ├── source_range.rs └── text_modifications.rs └── tests ├── cat.sh ├── fixtures ├── common-mark.md ├── definition-list.md ├── heading-id-classes.md ├── indented-code-block.md ├── lists-nested.md ├── math.md ├── nested.md ├── ordered.md ├── repeated-shortcut-links.md ├── snapshots │ ├── stupicat-definition-list-output │ ├── stupicat-event-by-event-output │ ├── stupicat-heading-id-classes-output │ ├── stupicat-indented-code-block │ ├── stupicat-lists-nested-output │ ├── stupicat-math-output │ ├── stupicat-nested-output │ ├── stupicat-ordered-output │ ├── stupicat-output │ ├── stupicat-repeated-shortcut-links-output │ ├── stupicat-super-sub-script-output │ ├── stupicat-table-output │ ├── stupicat-table-with-escapes-output │ ├── stupicat-table-with-html-output │ ├── stupicat-toml-frontmatter-output │ ├── stupicat-unordered-output │ └── stupicat-yaml-frontmatter-output ├── super-sub-script.md ├── table-with-escapes.md ├── table-with-html.md ├── table.md ├── toml-frontmatter.md ├── unordered.md └── yaml-frontmatter.md ├── integrate ├── display.rs ├── fmt.rs ├── main.rs └── spec.rs ├── spec └── CommonMark │ ├── LICENSE │ ├── README.google │ ├── smart_punct.txt │ └── spec.txt └── utilities.sh /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build-and-test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: cargo fmt 15 | run: | 16 | cargo fmt --check 17 | - name: tests 18 | run: | 19 | make tests 20 | 21 | msrv: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v4 25 | - uses: taiki-e/install-action@cargo-hack 26 | - run: cargo hack --rust-version --no-dev-deps check 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | /.docker-cargo-cache 4 | .DS_Store 5 | target/ 6 | /Cargo.lock 7 | sy-*.tar.gz 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ### How to contribute 2 | 3 | * [fork this project][fork] on github 4 | * For setting up the environment to run the self tests, look at `.github/workflows/rust.yml`. 5 | * **Write a test that fails unless your patch is present.** 6 | * There are fixture-based tests run by [`cat.sh`][sh-tests]. 7 | * There are [unit-level tests][unit-tests] run by `cargo test`. 8 | * **Write the patch to fix the test**. 9 | * Add yourself to the `authors` line in the [`Cargo.toml`][cargo-authors] file. 10 | * Initiate a pull request 11 | 12 | [fork]: https://github.com/Byron/pulldown-cmark-to-cmark/fork 13 | [cargo-authors]: https://github.com/Byron/pulldown-cmark-to-cmark/blob/master/Cargo.toml#L4 14 | [unit-tests]: https://github.com/Byron/pulldown-cmark-to-cmark/blob/76667725b61be24890fbdfed5e7ecdb4c1ad1dc8/tests/fmt.rs#L146 15 | [sh-tests]: https://github.com/Byron/pulldown-cmark-to-cmark/blob/76667725b61be24890fbdfed5e7ecdb4c1ad1dc8/tests/cat.sh#L16 16 | 17 | ### CommonMark Conformance Results 18 | 19 | The `cargo test` suite will automatically test that `pulldown-cmark-to-cmark` 20 | has maintained its expected conformance level against the example Markdown 21 | programs in the CommonMark specification. As of time of writing, 22 | **`pulldown-cmark-to-cmark` passes ~90%** of the CommonMark example program tests. 23 | 24 | We would love your help in improving our conformance! 🙂 25 | 26 | To view which conformance tests could use your help to fix, run: 27 | 28 | ```shell 29 | $ FULL_CMARK_RESULTS=true cargo test 30 | ``` 31 | 32 | This will show a visual report of each failing CommonMark conformance test, 33 | including the original Markdown, the Markdown generated by 34 | `pulldown-cmark-to-cmark`, the expected HTML result, and a diff of the `Event` 35 | streams of the original and regenerated Markdown. 36 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pulldown-cmark-to-cmark" 3 | version = "21.0.0" 4 | authors = [ 5 | "Sebastian Thiel ", 6 | "Dylan Owen ", 7 | "Alessandro Ogier ", 8 | "Zixian Cai <2891235+caizixian@users.noreply.github.com>", 9 | "Andrew Lyjak "] 10 | 11 | description = "Convert pulldown-cmark Events back to the string they were parsed from" 12 | license = "Apache-2.0" 13 | keywords = ["markdown", "common-mark", "render", "converter"] 14 | repository = "https://github.com/Byron/pulldown-cmark-to-cmark" 15 | homepage = "https://github.com/Byron/pulldown-cmark-to-cmark" 16 | documentation = "https://docs.rs/crate/pulldown-cmark-to-cmark" 17 | readme = "README.md" 18 | edition = "2018" 19 | include = ["src/*.rs", "LICENSE-APACHE", "README.md", "CHANGELOG.md"] 20 | # This follows the MSRV of `pulldown-cmark` 21 | rust-version = "1.71.1" 22 | 23 | [dependencies] 24 | pulldown-cmark = { version = "0.13.0", default-features = false } 25 | 26 | [dev-dependencies] 27 | indoc = "2.0.5" 28 | pretty_assertions = "1.4.0" 29 | yansi = "1.0.1" 30 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018 "Sebastian Thiel ", "Dylan Owen ", "Alessandro Ogier ", "Zixian Cai <2891235+caizixian@users.noreply.github.com>" 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | help: ## Display this help 3 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 4 | 5 | .PHONY: docs unittests journeytests tests 6 | 7 | ##@ Testing 8 | 9 | docs: ## Builds docs 10 | cargo doc 11 | 12 | unittests: ## run unit tests 13 | cargo test --all 14 | 15 | clippy: ## run clippy 16 | cargo clippy 17 | 18 | journeytests: ## run journey tests 19 | ./tests/cat.sh 20 | 21 | 22 | tests: docs clippy unittests journeytests ## run all tests 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Crates.io](https://img.shields.io/crates/v/pulldown-cmark-to-cmark)](https://crates.io/crates/pulldown-cmark-to-cmark) 2 | ![Rust](https://github.com/Byron/pulldown-cmark-to-cmark/workflows/Rust/badge.svg) 3 | 4 | A utility library which translates [`Event`][pdcm-event] back to markdown. 5 | It's the prerequisite for writing markdown filters which can work as 6 | [mdbook-preprocessors][mdbook-prep]. 7 | 8 | This library takes great pride in supporting **everything that `pulldown-cmark`** supports, 9 | including *tables* and *footnotes* and *codeblocks in codeblocks*, 10 | while assuring *quality* with a powerful test suite. 11 | 12 | [pdcm-event]: https://docs.rs/pulldown-cmark/latest/pulldown_cmark/enum.Event.html 13 | [mdbook-prep]: https://rust-lang.github.io/mdBook/for_developers/preprocessors.html 14 | 15 | ### How to use 16 | 17 | Please have a look at the [`stupicat`-example][sc-example] for a complete tour 18 | of the API, or have a look at the [api-docs][api]. 19 | 20 | It's easiest to get this library into your `Cargo.toml` using `cargo-add`: 21 | ``` 22 | cargo add pulldown-cmark-to-cmark 23 | ``` 24 | 25 | [sc-example]: https://github.com/Byron/pulldown-cmark-to-cmark/blob/76667725b61be24890fbdfed5e7ecdb4c1ad1dc8/examples/stupicat.rs#L21 26 | [api]: https://docs.rs/crate/pulldown-cmark-to-cmark 27 | 28 | ### Supported Rust Versions 29 | 30 | `pulldown-cmark-to-cmark` follows the MSRV (minimum supported rust version) policy of [`pulldown-cmark`]. The current MSRV is 1.71.1. 31 | 32 | [`pulldown-cmark`]: https://github.com/pulldown-cmark/pulldown-cmark 33 | 34 | ### Friends of this project 35 | 36 | * [**termbook**](https://github.com/Byron/termbook) 37 | * A runner for `mdbooks` to keep your documentation tested. 38 | * [**Share Secrets Safely**](https://github.com/Byron/share-secrets-safely) 39 | * share secrets within teams to avoid plain-text secrets from day one 40 | 41 | ### Maintenance Guide 42 | 43 | #### Making a new release 44 | 45 | * **Assure all documentation is up-to-date and tests are green** 46 | * update the `version` in `Cargo.toml` and `git commit` 47 | * run `cargo release --no-dev-version` 48 | -------------------------------------------------------------------------------- /examples/stupicat.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | env, 3 | io::{stdout, Write}, 4 | }; 5 | 6 | use pulldown_cmark::{Options, Parser}; 7 | use pulldown_cmark_to_cmark::{cmark, cmark_resume}; 8 | 9 | fn main() -> Result<(), Box> { 10 | let path = env::args_os() 11 | .nth(1) 12 | .expect("First argument is markdown file to display"); 13 | let event_by_event = env::var_os("STUPICAT_STATE_TEST").is_some(); 14 | 15 | let md = std::fs::read_to_string(&path)?; 16 | let mut buf = String::with_capacity(md.len() + 128); 17 | let mut options = Options::all(); 18 | options.remove(Options::ENABLE_SMART_PUNCTUATION); 19 | 20 | if event_by_event { 21 | let mut state = None; 22 | for event in Parser::new_ext(&md, options) { 23 | state = cmark_resume(std::iter::once(event), &mut buf, state.take())?.into(); 24 | } 25 | if let Some(state) = state { 26 | state.finalize(&mut buf)?; 27 | } 28 | } else { 29 | cmark(Parser::new_ext(&md, options), &mut buf)?; 30 | } 31 | 32 | stdout().write_all(buf.as_bytes())?; 33 | Ok(()) 34 | } 35 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 120 2 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(rust_2018_idioms)] 2 | 3 | use std::{ 4 | borrow::{Borrow, Cow}, 5 | collections::HashSet, 6 | fmt, 7 | ops::Range, 8 | }; 9 | 10 | use pulldown_cmark::{Alignment as TableAlignment, BlockQuoteKind, Event, LinkType, MetadataBlockKind, Tag, TagEnd}; 11 | 12 | mod source_range; 13 | mod text_modifications; 14 | 15 | pub use source_range::{ 16 | cmark_resume_with_source_range, cmark_resume_with_source_range_and_options, cmark_with_source_range, 17 | cmark_with_source_range_and_options, 18 | }; 19 | use text_modifications::*; 20 | 21 | /// Similar to [Pulldown-Cmark-Alignment][Alignment], but with required 22 | /// traits for comparison to allow testing. 23 | #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 24 | pub enum Alignment { 25 | None, 26 | Left, 27 | Center, 28 | Right, 29 | } 30 | 31 | impl<'a> From<&'a TableAlignment> for Alignment { 32 | fn from(s: &'a TableAlignment) -> Self { 33 | match *s { 34 | TableAlignment::None => Self::None, 35 | TableAlignment::Left => Self::Left, 36 | TableAlignment::Center => Self::Center, 37 | TableAlignment::Right => Self::Right, 38 | } 39 | } 40 | } 41 | 42 | #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 43 | pub enum CodeBlockKind { 44 | Indented, 45 | Fenced, 46 | } 47 | 48 | /// The state of the [`cmark_resume()`] and [`cmark_resume_with_options()`] functions. 49 | /// This does not only allow introspection, but enables the user 50 | /// to halt the serialization at any time, and resume it later. 51 | #[derive(Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 52 | #[non_exhaustive] 53 | pub struct State<'a> { 54 | /// The amount of newlines to insert after `Event::Start(...)` 55 | pub newlines_before_start: usize, 56 | /// The lists and their types for which we have seen a `Event::Start(List(...))` tag 57 | pub list_stack: Vec>, 58 | /// The computed padding and prefix to print after each newline. 59 | /// This changes with the level of `BlockQuote` and `List` events. 60 | pub padding: Vec>, 61 | /// Keeps the current table alignments, if we are currently serializing a table. 62 | pub table_alignments: Vec, 63 | /// Keeps the current table headers, if we are currently serializing a table. 64 | pub table_headers: Vec, 65 | /// The last seen text when serializing a header 66 | pub text_for_header: Option, 67 | /// Is set while we are handling text in a code block 68 | pub code_block: Option, 69 | /// True if the last event was text and the text does not have trailing newline. Used to inject additional newlines before code block end fence. 70 | pub last_was_text_without_trailing_newline: bool, 71 | /// True if the last event was a paragraph start. Used to escape spaces at start of line (prevent spurrious indented code). 72 | pub last_was_paragraph_start: bool, 73 | /// True if the next event is a link, image, or footnote. 74 | pub next_is_link_like: bool, 75 | /// Currently open links 76 | pub link_stack: Vec>, 77 | /// Currently open images 78 | pub image_stack: Vec>, 79 | /// Keeps track of the last seen heading's id, classes, and attributes 80 | pub current_heading: Option>, 81 | /// True whenever between `Start(TableCell)` and `End(TableCell)` 82 | pub in_table_cell: bool, 83 | 84 | /// Keeps track of the last seen shortcut/link 85 | pub current_shortcut_text: Option, 86 | /// A list of shortcuts seen so far for later emission 87 | pub shortcuts: Vec<(String, String, String)>, 88 | /// Index into the `source` bytes of the end of the range corresponding to the last event. 89 | /// 90 | /// It's used to see if the current event didn't capture some bytes because of a 91 | /// skipped-over backslash. 92 | pub last_event_end_index: usize, 93 | } 94 | 95 | #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 96 | pub enum LinkCategory<'a> { 97 | AngleBracketed, 98 | Reference { 99 | uri: Cow<'a, str>, 100 | title: Cow<'a, str>, 101 | id: Cow<'a, str>, 102 | }, 103 | Collapsed { 104 | uri: Cow<'a, str>, 105 | title: Cow<'a, str>, 106 | }, 107 | Shortcut { 108 | uri: Cow<'a, str>, 109 | title: Cow<'a, str>, 110 | }, 111 | Other { 112 | uri: Cow<'a, str>, 113 | title: Cow<'a, str>, 114 | }, 115 | } 116 | 117 | #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 118 | pub enum ImageLink<'a> { 119 | Reference { 120 | uri: Cow<'a, str>, 121 | title: Cow<'a, str>, 122 | id: Cow<'a, str>, 123 | }, 124 | Collapsed { 125 | uri: Cow<'a, str>, 126 | title: Cow<'a, str>, 127 | }, 128 | Shortcut { 129 | uri: Cow<'a, str>, 130 | title: Cow<'a, str>, 131 | }, 132 | Other { 133 | uri: Cow<'a, str>, 134 | title: Cow<'a, str>, 135 | }, 136 | } 137 | 138 | #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 139 | pub struct Heading<'a> { 140 | id: Option>, 141 | classes: Vec>, 142 | attributes: Vec<(Cow<'a, str>, Option>)>, 143 | } 144 | 145 | /// Thea mount of code-block tokens one needs to produce a valid fenced code-block. 146 | pub const DEFAULT_CODE_BLOCK_TOKEN_COUNT: usize = 3; 147 | 148 | /// Configuration for the [`cmark_with_options()`] and [`cmark_resume_with_options()`] functions. 149 | /// The defaults should provide decent spacing and most importantly, will 150 | /// provide a faithful rendering of your markdown document particularly when 151 | /// rendering it to HTML. 152 | /// 153 | /// It's best used with its `Options::default()` implementation. 154 | #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 155 | pub struct Options<'a> { 156 | pub newlines_after_headline: usize, 157 | pub newlines_after_paragraph: usize, 158 | pub newlines_after_codeblock: usize, 159 | pub newlines_after_htmlblock: usize, 160 | pub newlines_after_table: usize, 161 | pub newlines_after_rule: usize, 162 | pub newlines_after_list: usize, 163 | pub newlines_after_blockquote: usize, 164 | pub newlines_after_rest: usize, 165 | /// The amount of newlines placed after TOML or YAML metadata blocks at the beginning of a document. 166 | pub newlines_after_metadata: usize, 167 | /// Token count for fenced code block. An appropriate value of this field can be decided by 168 | /// [`calculate_code_block_token_count()`]. 169 | /// Note that the default value is `4` which allows for one level of nested code-blocks, 170 | /// which is typically a safe value for common kinds of markdown documents. 171 | pub code_block_token_count: usize, 172 | pub code_block_token: char, 173 | pub list_token: char, 174 | pub ordered_list_token: char, 175 | pub increment_ordered_list_bullets: bool, 176 | pub emphasis_token: char, 177 | pub strong_token: &'a str, 178 | } 179 | 180 | const DEFAULT_OPTIONS: Options<'_> = Options { 181 | newlines_after_headline: 2, 182 | newlines_after_paragraph: 2, 183 | newlines_after_codeblock: 2, 184 | newlines_after_htmlblock: 1, 185 | newlines_after_table: 2, 186 | newlines_after_rule: 2, 187 | newlines_after_list: 2, 188 | newlines_after_blockquote: 2, 189 | newlines_after_rest: 1, 190 | newlines_after_metadata: 1, 191 | code_block_token_count: 4, 192 | code_block_token: '`', 193 | list_token: '*', 194 | ordered_list_token: '.', 195 | increment_ordered_list_bullets: false, 196 | emphasis_token: '*', 197 | strong_token: "**", 198 | }; 199 | 200 | impl Default for Options<'_> { 201 | fn default() -> Self { 202 | DEFAULT_OPTIONS 203 | } 204 | } 205 | 206 | impl Options<'_> { 207 | pub fn special_characters(&self) -> Cow<'static, str> { 208 | // These always need to be escaped, even if reconfigured. 209 | const BASE: &str = "#\\_*<>`|[]"; 210 | if DEFAULT_OPTIONS.code_block_token == self.code_block_token 211 | && DEFAULT_OPTIONS.list_token == self.list_token 212 | && DEFAULT_OPTIONS.emphasis_token == self.emphasis_token 213 | && DEFAULT_OPTIONS.strong_token == self.strong_token 214 | { 215 | BASE.into() 216 | } else { 217 | let mut s = String::from(BASE); 218 | s.push(self.code_block_token); 219 | s.push(self.list_token); 220 | s.push(self.emphasis_token); 221 | s.push_str(self.strong_token); 222 | s.into() 223 | } 224 | } 225 | } 226 | 227 | /// The error returned by [`cmark_resume_one_event`] and 228 | /// [`cmark_resume_with_source_range_and_options`]. 229 | #[derive(Debug)] 230 | pub enum Error { 231 | FormatFailed(fmt::Error), 232 | UnexpectedEvent, 233 | } 234 | 235 | impl fmt::Display for Error { 236 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 237 | match self { 238 | Self::FormatFailed(e) => e.fmt(f), 239 | Self::UnexpectedEvent => f.write_str("Unexpected event while reconstructing Markdown"), 240 | } 241 | } 242 | } 243 | 244 | impl std::error::Error for Error {} 245 | 246 | impl From for Error { 247 | fn from(e: fmt::Error) -> Self { 248 | Self::FormatFailed(e) 249 | } 250 | } 251 | 252 | /// As [`cmark_with_options()`], but with default [`Options`]. 253 | pub fn cmark<'a, I, E, F>(events: I, mut formatter: F) -> Result, Error> 254 | where 255 | I: Iterator, 256 | E: Borrow>, 257 | F: fmt::Write, 258 | { 259 | cmark_with_options(events, &mut formatter, Default::default()) 260 | } 261 | 262 | /// As [`cmark_resume_with_options()`], but with default [`Options`]. 263 | pub fn cmark_resume<'a, I, E, F>(events: I, formatter: F, state: Option>) -> Result, Error> 264 | where 265 | I: Iterator, 266 | E: Borrow>, 267 | F: fmt::Write, 268 | { 269 | cmark_resume_with_options(events, formatter, state, Options::default()) 270 | } 271 | 272 | /// As [`cmark_resume_with_options()`], but with the [`State`] finalized. 273 | pub fn cmark_with_options<'a, I, E, F>(events: I, mut formatter: F, options: Options<'_>) -> Result, Error> 274 | where 275 | I: Iterator, 276 | E: Borrow>, 277 | F: fmt::Write, 278 | { 279 | let state = cmark_resume_with_options(events, &mut formatter, Default::default(), options)?; 280 | state.finalize(formatter) 281 | } 282 | 283 | /// Serialize a stream of [pulldown-cmark-Events][Event] into a string-backed buffer. 284 | /// 285 | /// 1. **events** 286 | /// * An iterator over [`Events`][Event], for example as returned by the [`Parser`][pulldown_cmark::Parser] 287 | /// 1. **formatter** 288 | /// * A format writer, can be a `String`. 289 | /// 1. **state** 290 | /// * The optional initial state of the serialization. 291 | /// 1. **options** 292 | /// * Customize the appearance of the serialization. All otherwise magic values are contained 293 | /// here. 294 | /// 295 | /// *Returns* the [`State`] of the serialization on success. You can use it as initial state in the 296 | /// next call if you are halting event serialization. 297 | /// 298 | /// *Errors* if the underlying buffer fails (which is unlikely) or if the [`Event`] stream 299 | /// cannot ever be produced by deserializing valid Markdown. Each failure mode corresponds to one 300 | /// of [`Error`]'s variants. 301 | pub fn cmark_resume_with_options<'a, I, E, F>( 302 | events: I, 303 | mut formatter: F, 304 | state: Option>, 305 | options: Options<'_>, 306 | ) -> Result, Error> 307 | where 308 | I: Iterator, 309 | E: Borrow>, 310 | F: fmt::Write, 311 | { 312 | let mut state = state.unwrap_or_default(); 313 | let mut events = events.peekable(); 314 | while let Some(event) = events.next() { 315 | state.next_is_link_like = matches!( 316 | events.peek().map(Borrow::borrow), 317 | Some( 318 | Event::Start(Tag::Link { .. } | Tag::Image { .. } | Tag::FootnoteDefinition(..)) 319 | | Event::FootnoteReference(..) 320 | ) 321 | ); 322 | cmark_resume_one_event(event, &mut formatter, &mut state, &options)?; 323 | } 324 | Ok(state) 325 | } 326 | 327 | fn cmark_resume_one_event<'a, E, F>( 328 | event: E, 329 | formatter: &mut F, 330 | state: &mut State<'a>, 331 | options: &Options<'_>, 332 | ) -> Result<(), Error> 333 | where 334 | E: Borrow>, 335 | F: fmt::Write, 336 | { 337 | use pulldown_cmark::{Event::*, Tag::*}; 338 | 339 | let last_was_text_without_trailing_newline = state.last_was_text_without_trailing_newline; 340 | state.last_was_text_without_trailing_newline = false; 341 | let last_was_paragraph_start = state.last_was_paragraph_start; 342 | state.last_was_paragraph_start = false; 343 | 344 | let res = match event.borrow() { 345 | Rule => { 346 | consume_newlines(formatter, state)?; 347 | state.set_minimum_newlines_before_start(options.newlines_after_rule); 348 | formatter.write_str("---") 349 | } 350 | Code(text) => { 351 | if let Some(shortcut_text) = state.current_shortcut_text.as_mut() { 352 | shortcut_text.push('`'); 353 | shortcut_text.push_str(text); 354 | shortcut_text.push('`'); 355 | } 356 | if let Some(text_for_header) = state.text_for_header.as_mut() { 357 | text_for_header.push('`'); 358 | text_for_header.push_str(text); 359 | text_for_header.push('`'); 360 | } 361 | 362 | // (re)-escape `|` when it appears as part of inline code in the 363 | // body of a table. 364 | // 365 | // NOTE: This does not do *general* escaped-character handling 366 | // because the only character which *requires* this handling in this 367 | // spot in earlier versions of `pulldown-cmark` is a pipe character 368 | // in inline code in a table. Other escaping is handled when `Text` 369 | // events are emitted. 370 | let text = if state.in_table_cell { 371 | Cow::Owned(text.replace('|', "\\|")) 372 | } else { 373 | Cow::Borrowed(text.as_ref()) 374 | }; 375 | 376 | // When inline code has leading and trailing ' ' characters, additional space is needed 377 | // to escape it, unless all characters are space. 378 | if text.chars().all(|ch| ch == ' ') { 379 | write!(formatter, "`{text}`") 380 | } else { 381 | // More backticks are needed to delimit the inline code than the maximum number of 382 | // backticks in a consecutive run. 383 | let backticks = Repeated('`', max_consecutive_chars(&text, '`') + 1); 384 | let space = match text.as_bytes() { 385 | &[b'`', ..] | &[.., b'`'] => " ", // Space needed to separate backtick. 386 | &[b' ', .., b' '] => " ", // Space needed to escape inner space. 387 | _ => "", // No space needed. 388 | }; 389 | write!(formatter, "{backticks}{space}{text}{space}{backticks}") 390 | } 391 | } 392 | Start(tag) => { 393 | if let List(list_type) = tag { 394 | state.list_stack.push(*list_type); 395 | if state.list_stack.len() > 1 { 396 | state.set_minimum_newlines_before_start(options.newlines_after_rest); 397 | } 398 | } 399 | let consumed_newlines = state.newlines_before_start != 0; 400 | consume_newlines(formatter, state)?; 401 | match tag { 402 | Item => { 403 | // lazy lists act like paragraphs with no event 404 | state.last_was_paragraph_start = true; 405 | match state.list_stack.last_mut() { 406 | Some(inner) => { 407 | state.padding.push(list_item_padding_of(*inner)); 408 | match inner { 409 | Some(n) => { 410 | let bullet_number = *n; 411 | if options.increment_ordered_list_bullets { 412 | *n += 1; 413 | } 414 | write!(formatter, "{}{} ", bullet_number, options.ordered_list_token) 415 | } 416 | None => write!(formatter, "{} ", options.list_token), 417 | } 418 | } 419 | None => Ok(()), 420 | } 421 | } 422 | Table(alignments) => { 423 | state.table_alignments = alignments.iter().map(From::from).collect(); 424 | Ok(()) 425 | } 426 | TableHead => Ok(()), 427 | TableRow => Ok(()), 428 | TableCell => { 429 | state.text_for_header = Some(String::new()); 430 | state.in_table_cell = true; 431 | formatter.write_char('|') 432 | } 433 | Link { 434 | link_type, 435 | dest_url, 436 | title, 437 | id, 438 | } => { 439 | state.link_stack.push(match link_type { 440 | LinkType::Autolink | LinkType::Email => { 441 | formatter.write_char('<')?; 442 | LinkCategory::AngleBracketed 443 | } 444 | LinkType::Reference => { 445 | formatter.write_char('[')?; 446 | LinkCategory::Reference { 447 | uri: dest_url.clone().into(), 448 | title: title.clone().into(), 449 | id: id.clone().into(), 450 | } 451 | } 452 | LinkType::Collapsed => { 453 | state.current_shortcut_text = Some(String::new()); 454 | formatter.write_char('[')?; 455 | LinkCategory::Collapsed { 456 | uri: dest_url.clone().into(), 457 | title: title.clone().into(), 458 | } 459 | } 460 | LinkType::Shortcut => { 461 | state.current_shortcut_text = Some(String::new()); 462 | formatter.write_char('[')?; 463 | LinkCategory::Shortcut { 464 | uri: dest_url.clone().into(), 465 | title: title.clone().into(), 466 | } 467 | } 468 | _ => { 469 | formatter.write_char('[')?; 470 | LinkCategory::Other { 471 | uri: dest_url.clone().into(), 472 | title: title.clone().into(), 473 | } 474 | } 475 | }); 476 | Ok(()) 477 | } 478 | Image { 479 | link_type, 480 | dest_url, 481 | title, 482 | id, 483 | } => { 484 | state.image_stack.push(match link_type { 485 | LinkType::Reference => ImageLink::Reference { 486 | uri: dest_url.clone().into(), 487 | title: title.clone().into(), 488 | id: id.clone().into(), 489 | }, 490 | LinkType::Collapsed => { 491 | state.current_shortcut_text = Some(String::new()); 492 | ImageLink::Collapsed { 493 | uri: dest_url.clone().into(), 494 | title: title.clone().into(), 495 | } 496 | } 497 | LinkType::Shortcut => { 498 | state.current_shortcut_text = Some(String::new()); 499 | ImageLink::Shortcut { 500 | uri: dest_url.clone().into(), 501 | title: title.clone().into(), 502 | } 503 | } 504 | _ => ImageLink::Other { 505 | uri: dest_url.clone().into(), 506 | title: title.clone().into(), 507 | }, 508 | }); 509 | formatter.write_str("![") 510 | } 511 | Emphasis => formatter.write_char(options.emphasis_token), 512 | Strong => formatter.write_str(options.strong_token), 513 | FootnoteDefinition(name) => { 514 | state.padding.push(" ".into()); 515 | write!(formatter, "[^{name}]: ") 516 | } 517 | Paragraph => { 518 | state.last_was_paragraph_start = true; 519 | Ok(()) 520 | } 521 | Heading { 522 | level, 523 | id, 524 | classes, 525 | attrs, 526 | } => { 527 | if state.current_heading.is_some() { 528 | return Err(Error::UnexpectedEvent); 529 | } 530 | state.current_heading = Some(self::Heading { 531 | id: id.as_ref().map(|id| id.clone().into()), 532 | classes: classes.iter().map(|class| class.clone().into()).collect(), 533 | attributes: attrs 534 | .iter() 535 | .map(|(k, v)| (k.clone().into(), v.as_ref().map(|val| val.clone().into()))) 536 | .collect(), 537 | }); 538 | // Write '#', '##', '###', etc. based on the heading level. 539 | write!(formatter, "{} ", Repeated('#', *level as usize)) 540 | } 541 | BlockQuote(kind) => { 542 | let every_line_padding = " > "; 543 | let first_line_padding = kind 544 | .map(|kind| match kind { 545 | BlockQuoteKind::Note => " > [!NOTE]", 546 | BlockQuoteKind::Tip => " > [!TIP]", 547 | BlockQuoteKind::Important => " > [!IMPORTANT]", 548 | BlockQuoteKind::Warning => " > [!WARNING]", 549 | BlockQuoteKind::Caution => " > [!CAUTION]", 550 | }) 551 | .unwrap_or(every_line_padding); 552 | state.newlines_before_start = 1; 553 | 554 | // if we consumed some newlines, we know that we can just write out the next 555 | // level in our blockquote. This should work regardless if we have other 556 | // padding or if we're in a list 557 | if !consumed_newlines { 558 | write_padded_newline(formatter, state)?; 559 | } 560 | formatter.write_str(first_line_padding)?; 561 | state.padding.push(every_line_padding.into()); 562 | Ok(()) 563 | } 564 | CodeBlock(pulldown_cmark::CodeBlockKind::Indented) => { 565 | state.code_block = Some(CodeBlockKind::Indented); 566 | state.padding.push(" ".into()); 567 | if consumed_newlines { 568 | formatter.write_str(" ") 569 | } else { 570 | write_padded_newline(formatter, &state) 571 | } 572 | } 573 | CodeBlock(pulldown_cmark::CodeBlockKind::Fenced(info)) => { 574 | state.code_block = Some(CodeBlockKind::Fenced); 575 | if !consumed_newlines { 576 | write_padded_newline(formatter, &state)?; 577 | } 578 | 579 | let fence = Repeated(options.code_block_token, options.code_block_token_count); 580 | write!(formatter, "{fence}{info}")?; 581 | write_padded_newline(formatter, &state) 582 | } 583 | HtmlBlock => Ok(()), 584 | MetadataBlock(MetadataBlockKind::YamlStyle) => formatter.write_str("---\n"), 585 | MetadataBlock(MetadataBlockKind::PlusesStyle) => formatter.write_str("+++\n"), 586 | List(_) => Ok(()), 587 | Strikethrough => formatter.write_str("~~"), 588 | DefinitionList => Ok(()), 589 | DefinitionListTitle => { 590 | state.set_minimum_newlines_before_start(options.newlines_after_rest); 591 | Ok(()) 592 | } 593 | DefinitionListDefinition => { 594 | let every_line_padding = " "; 595 | let first_line_padding = ": "; 596 | 597 | padding(formatter, &state.padding).and(formatter.write_str(first_line_padding))?; 598 | state.padding.push(every_line_padding.into()); 599 | Ok(()) 600 | } 601 | Superscript => formatter.write_str(""), 602 | Subscript => formatter.write_str(""), 603 | } 604 | } 605 | End(tag) => match tag { 606 | TagEnd::Link => match if let Some(link_cat) = state.link_stack.pop() { 607 | link_cat 608 | } else { 609 | return Err(Error::UnexpectedEvent); 610 | } { 611 | LinkCategory::AngleBracketed => formatter.write_char('>'), 612 | LinkCategory::Reference { uri, title, id } => { 613 | state 614 | .shortcuts 615 | .push((id.to_string(), uri.to_string(), title.to_string())); 616 | formatter.write_str("][")?; 617 | formatter.write_str(&id)?; 618 | formatter.write_char(']') 619 | } 620 | LinkCategory::Collapsed { uri, title } => { 621 | if let Some(shortcut_text) = state.current_shortcut_text.take() { 622 | state 623 | .shortcuts 624 | .push((shortcut_text, uri.to_string(), title.to_string())); 625 | } 626 | formatter.write_str("][]") 627 | } 628 | LinkCategory::Shortcut { uri, title } => { 629 | if let Some(shortcut_text) = state.current_shortcut_text.take() { 630 | state 631 | .shortcuts 632 | .push((shortcut_text, uri.to_string(), title.to_string())); 633 | } 634 | formatter.write_char(']') 635 | } 636 | LinkCategory::Other { uri, title } => close_link(&uri, &title, formatter, LinkType::Inline), 637 | }, 638 | TagEnd::Image => match if let Some(img_link) = state.image_stack.pop() { 639 | img_link 640 | } else { 641 | return Err(Error::UnexpectedEvent); 642 | } { 643 | ImageLink::Reference { uri, title, id } => { 644 | state 645 | .shortcuts 646 | .push((id.to_string(), uri.to_string(), title.to_string())); 647 | formatter.write_str("][")?; 648 | formatter.write_str(&id)?; 649 | formatter.write_char(']') 650 | } 651 | ImageLink::Collapsed { uri, title } => { 652 | if let Some(shortcut_text) = state.current_shortcut_text.take() { 653 | state 654 | .shortcuts 655 | .push((shortcut_text, uri.to_string(), title.to_string())); 656 | } 657 | formatter.write_str("][]") 658 | } 659 | ImageLink::Shortcut { uri, title } => { 660 | if let Some(shortcut_text) = state.current_shortcut_text.take() { 661 | state 662 | .shortcuts 663 | .push((shortcut_text, uri.to_string(), title.to_string())); 664 | } 665 | formatter.write_char(']') 666 | } 667 | ImageLink::Other { uri, title } => { 668 | close_link(uri.as_ref(), title.as_ref(), formatter, LinkType::Inline) 669 | } 670 | }, 671 | TagEnd::Emphasis => formatter.write_char(options.emphasis_token), 672 | TagEnd::Strong => formatter.write_str(options.strong_token), 673 | TagEnd::Heading(_) => { 674 | let Some(self::Heading { 675 | id, 676 | classes, 677 | attributes, 678 | }) = state.current_heading.take() 679 | else { 680 | return Err(Error::UnexpectedEvent); 681 | }; 682 | let emit_braces = id.is_some() || !classes.is_empty() || !attributes.is_empty(); 683 | if emit_braces { 684 | formatter.write_str(" {")?; 685 | } 686 | if let Some(id_str) = id { 687 | formatter.write_char(' ')?; 688 | formatter.write_char('#')?; 689 | formatter.write_str(&id_str)?; 690 | } 691 | for class in &classes { 692 | formatter.write_char(' ')?; 693 | formatter.write_char('.')?; 694 | formatter.write_str(class)?; 695 | } 696 | for (key, val) in &attributes { 697 | formatter.write_char(' ')?; 698 | formatter.write_str(key)?; 699 | if let Some(val) = val { 700 | formatter.write_char('=')?; 701 | formatter.write_str(val)?; 702 | } 703 | } 704 | if emit_braces { 705 | formatter.write_char(' ')?; 706 | formatter.write_char('}')?; 707 | } 708 | state.set_minimum_newlines_before_start(options.newlines_after_headline); 709 | Ok(()) 710 | } 711 | TagEnd::Paragraph => { 712 | state.set_minimum_newlines_before_start(options.newlines_after_paragraph); 713 | Ok(()) 714 | } 715 | TagEnd::CodeBlock => { 716 | state.set_minimum_newlines_before_start(options.newlines_after_codeblock); 717 | if last_was_text_without_trailing_newline { 718 | write_padded_newline(formatter, &state)?; 719 | } 720 | match state.code_block { 721 | Some(CodeBlockKind::Fenced) => { 722 | let fence = Repeated(options.code_block_token, options.code_block_token_count); 723 | write!(formatter, "{fence}")?; 724 | } 725 | Some(CodeBlockKind::Indented) => { 726 | state.padding.pop(); 727 | } 728 | None => {} 729 | } 730 | state.code_block = None; 731 | Ok(()) 732 | } 733 | TagEnd::HtmlBlock => { 734 | state.set_minimum_newlines_before_start(options.newlines_after_htmlblock); 735 | Ok(()) 736 | } 737 | TagEnd::MetadataBlock(MetadataBlockKind::PlusesStyle) => { 738 | state.set_minimum_newlines_before_start(options.newlines_after_metadata); 739 | formatter.write_str("+++\n") 740 | } 741 | TagEnd::MetadataBlock(MetadataBlockKind::YamlStyle) => { 742 | state.set_minimum_newlines_before_start(options.newlines_after_metadata); 743 | formatter.write_str("---\n") 744 | } 745 | TagEnd::Table => { 746 | state.set_minimum_newlines_before_start(options.newlines_after_table); 747 | state.table_alignments.clear(); 748 | state.table_headers.clear(); 749 | Ok(()) 750 | } 751 | TagEnd::TableCell => { 752 | state 753 | .table_headers 754 | .push(state.text_for_header.take().unwrap_or_default()); 755 | state.in_table_cell = false; 756 | Ok(()) 757 | } 758 | t @ (TagEnd::TableRow | TagEnd::TableHead) => { 759 | state.set_minimum_newlines_before_start(options.newlines_after_rest); 760 | formatter.write_char('|')?; 761 | 762 | if let TagEnd::TableHead = t { 763 | write_padded_newline(formatter, &state)?; 764 | for (alignment, name) in state.table_alignments.iter().zip(state.table_headers.iter()) { 765 | formatter.write_char('|')?; 766 | // NOTE: For perfect counting, count grapheme clusters. 767 | // The reason this is not done is to avoid the dependency. 768 | 769 | // The minimum width of the column so that we can represent its alignment. 770 | let min_width = match alignment { 771 | // Must at least represent `-`. 772 | Alignment::None => 1, 773 | // Must at least represent `:-` or `-:` 774 | Alignment::Left | Alignment::Right => 2, 775 | // Must at least represent `:-:` 776 | Alignment::Center => 3, 777 | }; 778 | let length = name.chars().count().max(min_width); 779 | let last_minus_one = length.saturating_sub(1); 780 | for c in 0..length { 781 | formatter.write_char( 782 | if (c == 0 && (alignment == &Alignment::Center || alignment == &Alignment::Left)) 783 | || (c == last_minus_one 784 | && (alignment == &Alignment::Center || alignment == &Alignment::Right)) 785 | { 786 | ':' 787 | } else { 788 | '-' 789 | }, 790 | )?; 791 | } 792 | } 793 | formatter.write_char('|')?; 794 | } 795 | Ok(()) 796 | } 797 | TagEnd::Item => { 798 | state.padding.pop(); 799 | state.set_minimum_newlines_before_start(options.newlines_after_rest); 800 | Ok(()) 801 | } 802 | TagEnd::List(_) => { 803 | state.list_stack.pop(); 804 | if state.list_stack.is_empty() { 805 | state.set_minimum_newlines_before_start(options.newlines_after_list); 806 | } 807 | Ok(()) 808 | } 809 | TagEnd::BlockQuote(_) => { 810 | state.padding.pop(); 811 | 812 | state.set_minimum_newlines_before_start(options.newlines_after_blockquote); 813 | 814 | Ok(()) 815 | } 816 | TagEnd::FootnoteDefinition => { 817 | state.padding.pop(); 818 | Ok(()) 819 | } 820 | TagEnd::Strikethrough => formatter.write_str("~~"), 821 | TagEnd::DefinitionList => { 822 | state.set_minimum_newlines_before_start(options.newlines_after_list); 823 | Ok(()) 824 | } 825 | TagEnd::DefinitionListTitle => formatter.write_char('\n'), 826 | TagEnd::DefinitionListDefinition => { 827 | state.padding.pop(); 828 | write_padded_newline(formatter, &state) 829 | } 830 | TagEnd::Superscript => formatter.write_str(""), 831 | TagEnd::Subscript => formatter.write_str(""), 832 | }, 833 | HardBreak => formatter.write_str(" ").and(write_padded_newline(formatter, &state)), 834 | SoftBreak => write_padded_newline(formatter, &state), 835 | Text(text) => { 836 | let mut text = &text[..]; 837 | if let Some(shortcut_text) = state.current_shortcut_text.as_mut() { 838 | shortcut_text.push_str(text); 839 | } 840 | if let Some(text_for_header) = state.text_for_header.as_mut() { 841 | text_for_header.push_str(text); 842 | } 843 | consume_newlines(formatter, state)?; 844 | if last_was_paragraph_start { 845 | if text.starts_with('\t') { 846 | formatter.write_str(" ")?; 847 | text = &text[1..]; 848 | } else if text.starts_with(' ') { 849 | formatter.write_str(" ")?; 850 | text = &text[1..]; 851 | } 852 | } 853 | state.last_was_text_without_trailing_newline = !text.ends_with('\n'); 854 | let escaped_text = escape_special_characters(text, state, options); 855 | print_text_without_trailing_newline(&escaped_text, formatter, &state) 856 | } 857 | InlineHtml(text) => { 858 | consume_newlines(formatter, state)?; 859 | print_text_without_trailing_newline(text, formatter, &state) 860 | } 861 | Html(text) => { 862 | let mut lines = text.split('\n'); 863 | if let Some(line) = lines.next() { 864 | formatter.write_str(line)?; 865 | } 866 | for line in lines { 867 | write_padded_newline(formatter, &state)?; 868 | formatter.write_str(line)?; 869 | } 870 | Ok(()) 871 | } 872 | FootnoteReference(name) => write!(formatter, "[^{name}]"), 873 | TaskListMarker(checked) => { 874 | let check = if *checked { "x" } else { " " }; 875 | write!(formatter, "[{check}] ") 876 | } 877 | InlineMath(text) => write!(formatter, "${text}$"), 878 | DisplayMath(text) => write!(formatter, "$${text}$$"), 879 | }; 880 | 881 | Ok(res?) 882 | } 883 | 884 | impl State<'_> { 885 | pub fn finalize(mut self, mut formatter: F) -> Result 886 | where 887 | F: fmt::Write, 888 | { 889 | if self.shortcuts.is_empty() { 890 | return Ok(self); 891 | } 892 | 893 | formatter.write_str("\n")?; 894 | let mut written_shortcuts = HashSet::new(); 895 | for shortcut in self.shortcuts.drain(..) { 896 | if written_shortcuts.contains(&shortcut) { 897 | continue; 898 | } 899 | write!(formatter, "\n[{}", shortcut.0)?; 900 | close_link(&shortcut.1, &shortcut.2, &mut formatter, LinkType::Shortcut)?; 901 | written_shortcuts.insert(shortcut); 902 | } 903 | Ok(self) 904 | } 905 | 906 | pub fn is_in_code_block(&self) -> bool { 907 | self.code_block.is_some() 908 | } 909 | 910 | /// Ensure that [`State::newlines_before_start`] is at least as large as 911 | /// the provided option value. 912 | fn set_minimum_newlines_before_start(&mut self, option_value: usize) { 913 | if self.newlines_before_start < option_value { 914 | self.newlines_before_start = option_value 915 | } 916 | } 917 | } 918 | 919 | /// Return the ` + 1` that occur *within* a 920 | /// fenced code-block `events`. 921 | /// 922 | /// Use this function to obtain the correct value for `code_block_token_count` field of [`Options`] 923 | /// to assure that the enclosing code-blocks remain functional as such. 924 | /// 925 | /// Returns `None` if `events` didn't include any code-block, or the code-block didn't contain 926 | /// a nested block. In that case, the correct amount of fenced code-block tokens is 927 | /// [`DEFAULT_CODE_BLOCK_TOKEN_COUNT`]. 928 | /// 929 | /// ```rust 930 | /// use pulldown_cmark::Event; 931 | /// use pulldown_cmark_to_cmark::*; 932 | /// 933 | /// let events = &[Event::Text("text".into())]; 934 | /// let code_block_token_count = calculate_code_block_token_count(events).unwrap_or(DEFAULT_CODE_BLOCK_TOKEN_COUNT); 935 | /// let options = Options { 936 | /// code_block_token_count, 937 | /// ..Default::default() 938 | /// }; 939 | /// let mut buf = String::new(); 940 | /// cmark_with_options(events.iter(), &mut buf, options); 941 | /// ``` 942 | pub fn calculate_code_block_token_count<'a, I, E>(events: I) -> Option 943 | where 944 | I: IntoIterator, 945 | E: Borrow>, 946 | { 947 | let mut in_codeblock = false; 948 | let mut max_token_count = 0; 949 | 950 | // token_count should be taken over Text events 951 | // because a continuous text may be splitted to some Text events. 952 | let mut token_count = 0; 953 | let mut prev_token_char = None; 954 | for event in events { 955 | match event.borrow() { 956 | Event::Start(Tag::CodeBlock(_)) => { 957 | in_codeblock = true; 958 | } 959 | Event::End(TagEnd::CodeBlock) => { 960 | in_codeblock = false; 961 | prev_token_char = None; 962 | } 963 | Event::Text(x) if in_codeblock => { 964 | for c in x.chars() { 965 | let prev_token = prev_token_char.take(); 966 | if c == '`' || c == '~' { 967 | prev_token_char = Some(c); 968 | if Some(c) == prev_token { 969 | token_count += 1; 970 | } else { 971 | max_token_count = max_token_count.max(token_count); 972 | token_count = 1; 973 | } 974 | } 975 | } 976 | } 977 | _ => prev_token_char = None, 978 | } 979 | } 980 | 981 | max_token_count = max_token_count.max(token_count); 982 | (max_token_count >= 3).then_some(max_token_count + 1) 983 | } 984 | -------------------------------------------------------------------------------- /src/source_range.rs: -------------------------------------------------------------------------------- 1 | use super::{cmark_resume_one_event, fmt, Borrow, Error, Event, Options, Range, State}; 2 | 3 | /// Serialize a stream of [pulldown-cmark-Events][Event] while preserving the escape characters in `source`. 4 | /// Each input [Event] is accompanied by an optional [Range] that maps it back to the `source` string. 5 | /// 6 | /// Different from [`cmark_resume_with_options`](super::cmark_resume_with_options), which always escape 7 | /// Markdown special characters like `#` or `[`, this function only escapes a special character if 8 | /// it is escaped in `source`. 9 | /// 10 | /// 1. **source** 11 | /// * Markdown source from which `event_and_ranges` are created. 12 | /// 1. **event_and_ranges** 13 | /// * An iterator over [`Event`]-range pairs, for example as returned by [`pulldown_cmark::OffsetIter`]. 14 | /// Must match what's provided in `source`. 15 | /// 1. **formatter** 16 | /// * A format writer, can be a `String`. 17 | /// 1. **state** 18 | /// * The optional initial state of the serialization, useful when the operation should be resumed. 19 | /// 1. **options** 20 | /// * Customize the appearance of the serialization. All otherwise magic values are contained 21 | /// here. 22 | /// 23 | /// *Returns* the [`State`] of the serialization on success. You can use it as initial state in the 24 | /// next call if you are halting event serialization. 25 | /// 26 | /// *Errors* if the underlying buffer fails (which is unlikely) or if the [`Event`] stream 27 | /// iterated over by `event_and_ranges` cannot ever be produced by deserializing valid Markdown. 28 | /// Each failure mode corresponds to one of [`Error`]'s variants. 29 | pub fn cmark_resume_with_source_range_and_options<'a, I, E, F>( 30 | event_and_ranges: I, 31 | source: &'a str, 32 | mut formatter: F, 33 | state: Option>, 34 | options: Options<'_>, 35 | ) -> Result, Error> 36 | where 37 | I: Iterator>)>, 38 | E: Borrow>, 39 | F: fmt::Write, 40 | { 41 | let mut state = state.unwrap_or_default(); 42 | for (event, range) in event_and_ranges { 43 | let update_event_end_index = !matches!(*event.borrow(), Event::Start(_)); 44 | let prevent_escape_leading_special_characters = match (&range, event.borrow()) { 45 | // Headers and tables can have special characters that aren't at the start 46 | // of the line, because headers end with `#` and tables have pipes in the middle. 47 | _ if state.current_heading.is_some() || !state.table_alignments.is_empty() => false, 48 | // IMPORTANT: Any changes that allow anything other than `Text` 49 | // breaks the assumption below. 50 | (Some(range), Event::Text(_)) => { 51 | range.start <= state.last_event_end_index || 52 | // Some source characters are not captured, 53 | // so check the previous character. 54 | source.as_bytes().get(range.start.saturating_sub(1)) != Some(&b'\\') 55 | } 56 | _ => false, 57 | } && !state.is_in_code_block(); 58 | if prevent_escape_leading_special_characters { 59 | // Hack to not escape leading special characters. 60 | state.code_block = Some(crate::CodeBlockKind::Fenced); 61 | } 62 | cmark_resume_one_event(event, &mut formatter, &mut state, &options)?; 63 | if prevent_escape_leading_special_characters { 64 | // Assumption: this case only happens when `event` is `Text`, 65 | // so `state.is_in_code_block` should not be changed to `true`. 66 | // Also, `state.is_in_code_block` was `false`. 67 | state.code_block = None; 68 | } 69 | 70 | if let (true, Some(range)) = (update_event_end_index, range) { 71 | state.last_event_end_index = range.end; 72 | } 73 | } 74 | Ok(state) 75 | } 76 | 77 | /// As [`cmark_resume_with_source_range_and_options`], but with default [`Options`]. 78 | pub fn cmark_resume_with_source_range<'a, I, E, F>( 79 | event_and_ranges: I, 80 | source: &'a str, 81 | formatter: F, 82 | state: Option>, 83 | ) -> Result, Error> 84 | where 85 | I: Iterator>)>, 86 | E: Borrow>, 87 | F: fmt::Write, 88 | { 89 | cmark_resume_with_source_range_and_options(event_and_ranges, source, formatter, state, Options::default()) 90 | } 91 | 92 | /// As [`cmark_resume_with_source_range_and_options`], but with the [`State`] finalized. 93 | pub fn cmark_with_source_range_and_options<'a, I, E, F>( 94 | event_and_ranges: I, 95 | source: &'a str, 96 | mut formatter: F, 97 | options: Options<'_>, 98 | ) -> Result, Error> 99 | where 100 | I: Iterator>)>, 101 | E: Borrow>, 102 | F: fmt::Write, 103 | { 104 | let state = cmark_resume_with_source_range_and_options( 105 | event_and_ranges, 106 | source, 107 | &mut formatter, 108 | Default::default(), 109 | options, 110 | )?; 111 | state.finalize(formatter) 112 | } 113 | 114 | /// As [`cmark_with_source_range_and_options`], but with default [`Options`]. 115 | pub fn cmark_with_source_range<'a, I, E, F>( 116 | event_and_ranges: I, 117 | source: &'a str, 118 | mut formatter: F, 119 | ) -> Result, Error> 120 | where 121 | I: Iterator>)>, 122 | E: Borrow>, 123 | F: fmt::Write, 124 | { 125 | cmark_with_source_range_and_options(event_and_ranges, source, &mut formatter, Default::default()) 126 | } 127 | -------------------------------------------------------------------------------- /src/text_modifications.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | fmt::{self, Write}, 3 | Cow, LinkType, Options, State, 4 | }; 5 | 6 | /// Write a newline followed by the current [`State::padding`] 7 | /// text that indents the current nested content. 8 | /// 9 | /// [`write_padded_newline()`] takes care of writing both a newline character, 10 | /// and the appropriate padding characters, abstracting over the need to 11 | /// carefully pair those actions. 12 | /// 13 | /// # Purpose 14 | /// 15 | /// Consider a scenario where we're trying to write out the following Markdown 16 | /// (space indents visualized as '·'): 17 | /// 18 | /// ```markdown 19 | /// >·A block quote with an embedded list: 20 | /// >· 21 | /// >·* This is a list item that itself contains 22 | /// >···multiple lines and paragraphs of content. 23 | /// >··· 24 | /// >···Second paragraph. 25 | /// ``` 26 | /// 27 | /// Each line of output within the block quote needs to include the text `">·"` 28 | /// at the beginning of the line. Additionally, within the list, each line 29 | /// _also_ needs to start with `"··"` spaces so that the content of the 30 | /// list item is indented. 31 | /// 32 | /// Concretely, a call to [`write_padded_newline()`] after the first line in the 33 | /// paragraph of the list item would write `"\n>···"`. 34 | pub(crate) fn write_padded_newline(formatter: &mut impl fmt::Write, state: &State<'_>) -> Result<(), fmt::Error> { 35 | formatter.write_char('\n')?; 36 | padding(formatter, &state.padding)?; 37 | Ok(()) 38 | } 39 | 40 | pub(crate) fn padding(f: &mut F, p: &[Cow<'_, str>]) -> fmt::Result 41 | where 42 | F: fmt::Write, 43 | { 44 | for padding in p { 45 | write!(f, "{padding}")?; 46 | } 47 | Ok(()) 48 | } 49 | pub(crate) fn consume_newlines(f: &mut F, s: &mut State<'_>) -> fmt::Result 50 | where 51 | F: fmt::Write, 52 | { 53 | while s.newlines_before_start != 0 { 54 | s.newlines_before_start -= 1; 55 | write_padded_newline(f, s)?; 56 | } 57 | Ok(()) 58 | } 59 | 60 | pub(crate) fn print_text_without_trailing_newline(t: &str, f: &mut F, state: &State<'_>) -> fmt::Result 61 | where 62 | F: fmt::Write, 63 | { 64 | let line_count = t.split('\n').count(); 65 | for (tid, token) in t.split('\n').enumerate() { 66 | f.write_str(token)?; 67 | if tid + 1 < line_count { 68 | write_padded_newline(f, state)?; 69 | } 70 | } 71 | Ok(()) 72 | } 73 | 74 | pub(crate) fn list_item_padding_of(l: Option) -> Cow<'static, str> { 75 | match l { 76 | None => " ".into(), 77 | Some(n) => format!("{n}. ").chars().map(|_| ' ').collect::().into(), 78 | } 79 | } 80 | 81 | pub(crate) fn close_link(uri: &str, title: &str, f: &mut F, link_type: LinkType) -> fmt::Result 82 | where 83 | F: fmt::Write, 84 | { 85 | let needs_brackets = { 86 | let mut depth = 0; 87 | for b in uri.bytes() { 88 | match b { 89 | b'(' => depth += 1, 90 | b')' => depth -= 1, 91 | b' ' => { 92 | depth += 1; 93 | break; 94 | } 95 | _ => {} 96 | } 97 | if depth > 3 { 98 | break; 99 | } 100 | } 101 | depth != 0 102 | }; 103 | let separator = match link_type { 104 | LinkType::Shortcut => ": ", 105 | _ => "(", 106 | }; 107 | 108 | if needs_brackets { 109 | write!(f, "]{separator}<{uri}>")?; 110 | } else { 111 | write!(f, "]{separator}{uri}")?; 112 | } 113 | if !title.is_empty() { 114 | write!(f, " \"{title}\"", title = EscapeLinkTitle(title))?; 115 | } 116 | if link_type != LinkType::Shortcut { 117 | f.write_char(')')?; 118 | } 119 | 120 | Ok(()) 121 | } 122 | 123 | struct EscapeLinkTitle<'a>(&'a str); 124 | 125 | /// Writes a link title with double quotes escaped. 126 | /// See https://spec.commonmark.org/0.30/#link-title for the rules around 127 | /// link titles and the characters they may contain. 128 | impl fmt::Display for EscapeLinkTitle<'_> { 129 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 130 | for c in self.0.chars() { 131 | match c { 132 | '"' => f.write_str(r#"\""#)?, 133 | '\\' => f.write_str(r"\\")?, 134 | c => f.write_char(c)?, 135 | } 136 | } 137 | Ok(()) 138 | } 139 | } 140 | 141 | pub(crate) fn escape_special_characters<'a>(t: &'a str, state: &State<'a>, options: &Options<'a>) -> Cow<'a, str> { 142 | if state.is_in_code_block() || t.is_empty() { 143 | return Cow::Borrowed(t); 144 | } 145 | 146 | let first = t.chars().next().expect("at least one char"); 147 | let first_special = options.special_characters().contains(first); 148 | let ends_with_special = 149 | (state.next_is_link_like && t.ends_with("!")) || (state.current_heading.is_some() && t.ends_with("#")); 150 | let table_contains_pipe = !state.table_alignments.is_empty() && t.contains("|"); 151 | if first_special || ends_with_special || table_contains_pipe { 152 | let mut s = String::with_capacity(t.len() + 1); 153 | for (i, c) in t.char_indices() { 154 | if (i == 0 && first_special) || (i == t.len() - 1 && ends_with_special) || (c == '|' && table_contains_pipe) 155 | { 156 | s.push('\\'); 157 | } 158 | s.push(c); 159 | } 160 | Cow::Owned(s) 161 | } else { 162 | Cow::Borrowed(t) 163 | } 164 | } 165 | 166 | pub(crate) fn max_consecutive_chars(text: &str, search: char) -> usize { 167 | let mut in_search_chars = false; 168 | let mut max_count = 0; 169 | let mut cur_count = 0; 170 | 171 | for ch in text.chars() { 172 | if ch == search { 173 | cur_count += 1; 174 | in_search_chars = true; 175 | } else if in_search_chars { 176 | max_count = max_count.max(cur_count); 177 | cur_count = 0; 178 | in_search_chars = false; 179 | } 180 | } 181 | max_count.max(cur_count) 182 | } 183 | 184 | #[cfg(test)] 185 | mod max_consecutive_chars { 186 | use super::max_consecutive_chars; 187 | 188 | #[test] 189 | fn happens_in_the_entire_string() { 190 | assert_eq!( 191 | max_consecutive_chars("``a```b``", '`'), 192 | 3, 193 | "the highest seen consecutive segment of backticks counts" 194 | ); 195 | assert_eq!( 196 | max_consecutive_chars("```a``b`", '`'), 197 | 3, 198 | "it can't be downgraded later" 199 | ); 200 | } 201 | } 202 | 203 | //===================================== 204 | // General-purpose formatting utilities 205 | //===================================== 206 | 207 | /// `Repeated(content, count` formats as `content` repeated `count` times. 208 | #[derive(Debug)] 209 | pub(crate) struct Repeated(pub T, pub usize); 210 | 211 | impl fmt::Display for Repeated { 212 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 213 | let Repeated(content, count) = self; 214 | 215 | for _ in 0..*count { 216 | T::fmt(content, f)?; 217 | } 218 | Ok(()) 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /tests/cat.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eu -o pipefail 4 | exe=(cargo run --example stupicat --) 5 | 6 | root="$(cd "${0%/*}" && pwd)" 7 | # shellcheck source=./tests/utilities.sh 8 | source "$root/utilities.sh" 9 | 10 | SUCCESSFULLY=0 11 | 12 | fixture="$root/fixtures" 13 | snapshot="$fixture/snapshots" 14 | 15 | title "stupicat" 16 | 17 | (with "a mathematical expression" 18 | it "succeeds" && \ 19 | WITH_SNAPSHOT="$snapshot/stupicat-math-output" \ 20 | expect_run_sh $SUCCESSFULLY "${exe[*]} $fixture/math.md 2>/dev/null" 21 | ) 22 | 23 | (with "a table" 24 | it "succeeds" && \ 25 | WITH_SNAPSHOT="$snapshot/stupicat-table-output" \ 26 | expect_run_sh $SUCCESSFULLY "${exe[*]} $fixture/table.md 2>/dev/null" 27 | ) 28 | 29 | (with "a more complex ordered list" 30 | it "succeeds" && \ 31 | WITH_SNAPSHOT="$snapshot/stupicat-ordered-output" \ 32 | expect_run_sh $SUCCESSFULLY "${exe[*]} $fixture/ordered.md 2>/dev/null" 33 | ) 34 | 35 | (with "a more complex unordered list" 36 | it "succeeds" && \ 37 | WITH_SNAPSHOT="$snapshot/stupicat-unordered-output" \ 38 | expect_run_sh $SUCCESSFULLY "${exe[*]} $fixture/unordered.md 2>/dev/null" 39 | ) 40 | 41 | (with "a standard common-mark example file" 42 | (when "processing all events in one invocation" 43 | it "succeeds" && \ 44 | WITH_SNAPSHOT="$snapshot/stupicat-output" \ 45 | expect_run_sh $SUCCESSFULLY "${exe[*]} $fixture/common-mark.md 2>/dev/null" 46 | ) 47 | (when "processing event by event" 48 | it "succeeds" && \ 49 | STUPICAT_STATE_TEST=1 \ 50 | WITH_SNAPSHOT="$snapshot/stupicat-event-by-event-output" \ 51 | expect_run_sh $SUCCESSFULLY "${exe[*]} $fixture/common-mark.md 2>/dev/null" 52 | ) 53 | ) 54 | 55 | (with "markdown and html nested" 56 | it "succeeds" && \ 57 | WITH_SNAPSHOT="$snapshot/stupicat-nested-output" \ 58 | expect_run_sh $SUCCESSFULLY "${exe[*]} $fixture/nested.md 2>/dev/null" 59 | ) 60 | 61 | (with "lists and nested content" 62 | it "succeeds" && \ 63 | WITH_SNAPSHOT="$snapshot/stupicat-lists-nested-output" \ 64 | expect_run_sh $SUCCESSFULLY "${exe[*]} $fixture/lists-nested.md 2>/dev/null" 65 | ) 66 | 67 | (with "table with html" 68 | it "succeeds" && \ 69 | WITH_SNAPSHOT="$snapshot/stupicat-table-with-html-output" \ 70 | expect_run_sh $SUCCESSFULLY "${exe[*]} $fixture/table-with-html.md 2>/dev/null" 71 | ) 72 | 73 | (with "heading with identifier and classes" 74 | it "succeeds" && \ 75 | WITH_SNAPSHOT="$snapshot/stupicat-heading-id-classes-output" \ 76 | expect_run_sh $SUCCESSFULLY "${exe[*]} $fixture/heading-id-classes.md 2>/dev/null" 77 | ) 78 | 79 | (with "repeated shortcut links" 80 | it "succeeds" && \ 81 | WITH_SNAPSHOT="$snapshot/stupicat-repeated-shortcut-links-output" \ 82 | expect_run_sh $SUCCESSFULLY "${exe[*]} $fixture/repeated-shortcut-links.md 2>/dev/null" 83 | ) 84 | 85 | (with "indented code block" 86 | it "succeeds" && \ 87 | WITH_SNAPSHOT="$snapshot/stupicat-indented-code-block" \ 88 | expect_run_sh $SUCCESSFULLY "${exe[*]} $fixture/indented-code-block.md 2>/dev/null" 89 | ) 90 | 91 | (with "yaml frontmatter" 92 | it "succeeds" && \ 93 | WITH_SNAPSHOT="$snapshot/stupicat-yaml-frontmatter-output" \ 94 | expect_run_sh $SUCCESSFULLY "${exe[*]} $fixture/yaml-frontmatter.md 2>/dev/null" 95 | ) 96 | 97 | (with "toml frontmatter" 98 | it "succeeds" && \ 99 | WITH_SNAPSHOT="$snapshot/stupicat-toml-frontmatter-output" \ 100 | expect_run_sh $SUCCESSFULLY "${exe[*]} $fixture/toml-frontmatter.md 2>/dev/null" 101 | ) 102 | 103 | (with "table with escaped characters in cells" 104 | it "succeeds in reproducing the escapes" && \ 105 | WITH_SNAPSHOT="$snapshot/stupicat-table-with-escapes-output" \ 106 | expect_run_sh $SUCCESSFULLY "${exe[*]} $fixture/table-with-escapes.md 2>/dev/null" 107 | ) 108 | 109 | (with "definition lists" 110 | it "succeeds in reproducing definition-list indentation" && \ 111 | WITH_SNAPSHOT="$snapshot/stupicat-definition-list-output" \ 112 | expect_run_sh $SUCCESSFULLY "${exe[*]} $fixture/definition-list.md 2>/dev/null" 113 | ) 114 | 115 | (with "super and subscript" 116 | it "succeeds" && \ 117 | WITH_SNAPSHOT="$snapshot/stupicat-super-sub-script-output" \ 118 | expect_run_sh $SUCCESSFULLY "${exe[*]} $fixture/super-sub-script.md 2>/dev/null" 119 | ) 120 | -------------------------------------------------------------------------------- /tests/fixtures/common-mark.md: -------------------------------------------------------------------------------- 1 | # CommonMark sample document 2 | 3 | ## Basic inline formatting 4 | 5 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam **nonumy 6 | eirmod tempor invidunt** ut labore et *dolore magna aliquyam erat*, sed diam 7 | voluptua. `At vero eos et` accusam et 8 | 9 | ## Headers 10 | 11 | ### Level 3 12 | 13 | #### Level 4 14 | 15 | ##### Level 5 16 | 17 | ###### Level 6 18 | 19 | ## [Links] 20 | 21 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod 22 | tempor invidunt ut labore et dolore magna aliquyam erat 23 | (), sed [`diam`] [`voluptua`]. 24 | 25 | Lorem ipsum dolor sit amet, [consetetur 26 | sadipscing](http://www.example.com/inline) elitr, sed diam nonumy eirmod tempor 27 | invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos 28 | et accusam et [justo duo dolores][1] et ea rebum. Stet clita kasd gubergren, no 29 | sea [takimata sanctus](./showcase.md) est Lorem ipsum dolor sit amet. 30 | 31 | - [bacon](<(>) 32 | - [bacon](<)>) 33 | - [bacon](test()) 34 | 35 | Ask to . 36 | 37 | [1]: http://www.example.com/reference 38 | 39 | ## [Images] 40 | 41 | Images as blocks: 42 | 43 | ![The Rust logo](./rust-logo-128x128.png) 44 | 45 | ## Lists 46 | 47 | Unordered lists: 48 | 49 | * Lorem impsum 50 | * Nested 51 | * Inline 52 | * Text 53 | * dolor sit amet 54 | * Nested 55 | 56 | * With Paragraphs and nested blocks: 57 | 58 | > A quote 59 | 60 | And some text at the end 61 | * consetetur sadipscing elitr 62 | 63 | Ordered lists: 64 | 65 | 1. Lorem impsum 66 | 1. Nested 67 | 2. Inline 68 | 3. Text 69 | 2. dolor sit amet 70 | 1. Nested 71 | 72 | 2. With 73 | Paragraphs and nested blocks: 74 | 75 | > A quote 76 | 77 | And some text at the end 78 | 3. consetetur sadipscing elitr 79 | 80 | And a mix of both: 81 | 82 | * Lorem impsum 83 | 1. Nested 84 | 2. Inline 85 | * With 86 | * Some 87 | * Nested 88 | * Bullets 89 | 3. Text 90 | * dolor sit amet 91 | 92 | ## Block level elements 93 | 94 | Block quotes 95 | 96 | > Lorem ipsum dolor sit amet, *consetetur sadipscing elitr*, sed diam nonumy 97 | > eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam 98 | > voluptua. 99 | > 100 | > Lorem ipsum dolor sit amet, **consetetur sadipscing elitr**, sed diam nonumy 101 | > eirmod tempor invidunt ut `labore et dolore magna` aliquyam erat, sed diam 102 | > voluptua. 103 | 104 | > [!NOTE] 105 | > Highlights information that users should take into account, even when skimming. 106 | 107 | > [!TIP] 108 | > Optional information to help a user be more successful. 109 | 110 | > [!IMPORTANT] 111 | > Crucial information necessary for users to succeed. 112 | 113 | > [!WARNING] 114 | > Critical content demanding immediate user attention due to potential risks. 115 | 116 | > [!CAUTION] 117 | > Negative potential consequences of an action. 118 | 119 | Before we continue, have a ruler: 120 | 121 | ---- 122 | 123 | Code blocks without syntax highlighting: 124 | 125 | ``` 126 | Some plain 127 | code block 128 | fooo 129 | ``` 130 | 131 | Or with syntax highlighting, eg, Rust: 132 | 133 | ```rust 134 | fn main() { 135 | println!("Hello world") 136 | } 137 | ``` 138 | 139 | Or Haskell: 140 | 141 | ```haskell 142 | main :: IO () 143 | main = putStrLn "Hello World" 144 | ``` 145 | 146 | Or Scala: 147 | 148 | ```scala 149 | object HelloWorld { 150 | def main(args: Array[String]): Unit = { 151 | println("Hello, world!") 152 | } 153 | } 154 | ``` 155 | 156 | Or raw codeblocks: 157 | 158 | ```` 159 | ```bash 160 | echo 'hi from the innner codeblock' 161 | ``` 162 | ```` 163 | 164 | ## HTML 165 | 166 | We can have block html: 167 | 168 |
169 |

170 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod 171 | tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At 172 | vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, 173 | no sea takimata sanctus est Lorem ipsum dolor sit amet. 174 |

175 |
176 | 177 | Or inline HTML, as in this paragraph: Lorem ipsum dolor sit amet, consetetur 178 | sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore 179 | et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et 180 | justo duo dolores et ea rebum. Stet clita kasd gubergren, no 181 | sea takimata sanctus est Lorem ipsum dolor sit amet. 182 | 183 | ## Math 184 | 185 | This sentence uses `$` delimiters to show math inline: $\sqrt{3x-1}+(1+x)^2$ 186 | 187 | This sentence uses $\` and \`$ delimiters to show math inline: $`\sqrt{3x-1}+(1+x)^2`$ 188 | 189 | **The Cauchy-Schwarz Inequality** 190 | $$\left( \sum_{k=1}^n a_k b_k \right)^2 \leq \left( \sum_{k=1}^n a_k^2 \right) \left( \sum_{k=1}^n b_k^2 \right)$$ 191 | 192 | **The Cauchy-Schwarz Inequality** 193 | 194 | ```math 195 | \left( \sum_{k=1}^n a_k b_k \right)^2 \leq \left( \sum_{k=1}^n a_k^2 \right) \left( \sum_{k=1}^n b_k^2 \right) 196 | ``` 197 | 198 | This expression uses `\$` to display a dollar sign: $`\sqrt{\$4}`$ 199 | 200 | To split $100 in half, we calculate $100/2$ 201 | 202 | ## Escapes 203 | 204 | We now support escaping special characters, such as in \_\_init\_\_.py and in formulas 205 | like \\( \int x dx = \frac{x^2}{2} + C \\). 206 | 207 | What about \*this\* or \*\*that\*\*? 208 | 209 | \# heading? 210 | 211 | \[disabled inline link](target) 212 | 213 | \[disabled named link]: target 214 | 215 | [named-link][enabled] 216 | 217 | [enabled]: bah 218 | 219 | \[disabled named link]\[disabled] 220 | 221 | [disabled]: bah 222 | 223 | |table| 224 | \|-----| 225 | 226 | \`code` 227 | 228 | ] a closing bracket does nothing 229 | 230 | \ 231 | 232 | \> inline code blocks 233 | 234 | In code blocks, there will be no escaping: 235 | 236 | ```` 237 | A raw block inside 238 | ``` 239 | foo 240 | ``` 241 | 242 | * | < > # 243 | ```` 244 | 245 | ### Entity escapes 246 | 247 | © 2280 one world government, inc 248 | 249 | paragraph starts with a literal tab. 250 | 251 | I'm using markdown like a typewriter, 252 | probably because this document started life in a WYSIWYG 253 | editor and got converted... 254 | 255 | [Links]: http://www.example.com/shortcut 256 | [`diam`]: http://www.example.com/shortcut_code_diam 257 | [`voluptua`]: http://www.example.com/shortcut_code_voluptua 258 | [Images]: http://www.example.com/another_shortcut -------------------------------------------------------------------------------- /tests/fixtures/definition-list.md: -------------------------------------------------------------------------------- 1 | A paragraph 2 | 3 | A Term 4 | : Defined by block content 5 | 6 | - an embedded list entry 7 | - another entry 8 | 9 | Another Term 10 | : This one even has an embedded definition list 11 | 12 | A sub-term 13 | : It's definition 14 | 15 | Another sub-term 16 | : With a definition 17 | 18 | 19 | Back to a normal paragraph. 20 | 21 | --- 22 | 23 | > A quoted paragraph 24 | > 25 | > A quoted term 26 | > : its definition 27 | > 28 | > Another quoted term 29 | > : and its definition 30 | > 31 | -------------------------------------------------------------------------------- /tests/fixtures/heading-id-classes.md: -------------------------------------------------------------------------------- 1 | # nothing 2 | 3 | # with ID {#myh1} 4 | 5 | ## with class {.classh2} 6 | 7 | ### multiple {#myh3 .classh3} 8 | 9 | # text { #id .class1 .class2 myattr, other_attr=myvalue } 10 | -------------------------------------------------------------------------------- /tests/fixtures/indented-code-block.md: -------------------------------------------------------------------------------- 1 | codeblock: 2 | 3 | fn main() { 4 | println!("Hello, world!"); 5 | } -------------------------------------------------------------------------------- /tests/fixtures/lists-nested.md: -------------------------------------------------------------------------------- 1 | 1. list paragraph 1 2 | ``` 3 | code sample 4 | ``` 5 | 2. list paragraph 2 6 | 7 | --- 8 | 9 | 1. list paragraph 1 10 | ``` 11 | code sample 12 | ``` 13 | 14 | 2. list paragraph 2 15 | 16 | --- 17 | 18 | 1. list paragraph 1 19 | 20 | code sample 21 | 22 | 2. list paragraph 2 23 | -------------------------------------------------------------------------------- /tests/fixtures/math.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # `$`-delimited LaTeX Math in pulldown-cmark 4 | 5 | Mathematical expressions extension. Syntax based on 6 | . 7 | 8 | Inline mode mathematical expressions: 9 | 10 | This sentence uses `$` delimiters to show math inline: $\sqrt{3x-1}+(1+x)^2$ 11 | $\sum_{k=1}^n a_k b_k$: Mathematical expression at head of line 12 | 13 | Display mode mathematical expressions: 14 | 15 | **The Cauchy-Schwarz Inequality** 16 | 17 | $$\left( \sum_{k=1}^n a_k b_k \right)^2 \leq \left( \sum_{k=1}^n a_k^2 \right) \left( \sum_{k=1}^n b_k^2 \right)$$ 18 | 19 | Inline math expressions cannot be empty, but display mode expressions can. 20 | 21 | Oops empty $$ expression. 22 | 23 | $$$$ 24 | 25 | This is a greedy, left-to-right parser. 26 | 27 | $x$$$$$$$y$$ 28 | 29 | $x$$$$$$y$$ 30 | 31 | $$x$$$$$$y$$ 32 | 33 | Math expressions pass their content through as-is, ignoring any other inline 34 | Markdown constructs: 35 | 36 | $ac$ 37 | 38 | $${a*b*c} _c_ d$$ 39 | 40 | $not `code`$ 41 | 42 | $![not an](/image)$ 43 | 44 | $$ 45 | 46 | $α$ 47 | 48 | Sole `$` characters without a matching pair in the same block element 49 | are handled as normal text. 50 | 51 | Hello $world. 52 | 53 | Dollar at end of line$ 54 | 55 | Mathematical expressions can continue across multiple lines: 56 | 57 | $5x + 2 = 58 | 17$ 59 | 60 | $$\left( \sum_{k=1}^n a_k b_k \right)^2 \leq \left( \sum_{k=1}^n a_k^2 \right) 61 | \left( \sum_{k=1}^n b_k^2 \right)$$ 62 | 63 | Markdown hard breaks are also not recognized inside math expressions: 64 | 65 | $not a\ 66 | hard break 67 | either$ 68 | 69 | `$` character can be escaped with backslash in mathematical expressions: 70 | 71 | $\$$ 72 | 73 | $$y = \$ x$$ 74 | 75 | Inline mode math expressions cannot contain unescaped `$` characters. 76 | Neither can display math. 77 | 78 | $x $ x$ 79 | 80 | $$ $ $$ 81 | 82 | alpha$$beta$gamma$$delta 83 | 84 | Inline math expressions cannot start or end with whitespace, including newlines: 85 | 86 | these are not math texts: $ y=x$, $y=x $, $ 87 | y=x$ and $y=x 88 | $ 89 | 90 | >The start of a line counts as whitespace $2 + 91 | >$ 92 | 93 | While displays can start with whitespace, {${ 94 | they should not allow inlines to do that $$2 + 95 | $*$ 96 | 97 | Inline math expressions do not need to be surrounded with whitespace: 98 | 99 | these are math texts: foo$y=x$bar and $y=x$bar and foo$y=x$ bar 100 | 101 | Inline math expressions can be surrounded by punctuation: 102 | 103 | math texts: $x=y$! and $x=y$? and $x=y$: and $x=y$. and $x=y$" 104 | 105 | also math texts: !$x=y$! and ?$x=y$? and :$x=y$: and .$x=y$. and "$x=y$" 106 | 107 | braces: ($x=y$) [$x=y$] {$x=y$} 108 | 109 | Math expression as only item on a line: 110 | 111 | $x=y$ 112 | 113 | Math expressions can be immediately followed by other math expressions: 114 | 115 | $a$$b$ 116 | 117 | $a$$$b$$ 118 | 119 | $$a$$$b$ 120 | 121 | $$a$$$$b$$ 122 | 123 | Both inline and display mode math expressions are inline elements with the same 124 | precedence as code spans. The leftmost valid element takes priority: 125 | 126 | $Inline `first$ then` code 127 | 128 | `Code $first` then$ inline 129 | 130 | $$ Display `first $$ then` code 131 | 132 | `Code $$ first` then $$ display 133 | 134 | Indicators of block structure take precedence over math expressions: 135 | 136 | $x + y - z$ 137 | 138 | $x + y 139 | - z$ 140 | 141 | $$ x + y 142 | > z $$ 143 | 144 | This also means that math expressions cannot contain empty lines, since they 145 | start a new paragraph: 146 | 147 | $not 148 | 149 | math$ 150 | 151 | $$ 152 | not 153 | 154 | math 155 | $$ 156 | 157 | It also implies that math notation has lower 158 | parsing power than block elements. 159 | 160 | - $not 161 | - * 162 | math$ 163 | 164 | Note that math can contain embedded math. In scanning 165 | for a closing delimiter, we skip material in balanced 166 | curly braces: 167 | 168 | This is display math: 169 | $$ 170 | \text{Hello $x^2$} 171 | $$ 172 | And this is inline math: 173 | $\text{Hello $x$ there!}$ 174 | 175 | Math expressions must be nested within balanced curly braces. 176 | Backslash-escaped braces do not count. 177 | 178 | This is not valid math: $}{$ 179 | 180 | Neither is this: { $}{$ } 181 | 182 | This is: $\}\{$ 183 | 184 | This is: $\}$ 185 | 186 | Math environment contains 2+2: $}$2+2$ 187 | 188 | Math environment contains y: $x {$ $ } $y$ 189 | 190 | Math expressions must contain properly nested braces. 191 | 192 | This is not display math. It is inline math: 193 | 194 | $$\text{first $$ second}$ 195 | 196 | $$$\text{first $$ second}$ 197 | 198 | This is display math: 199 | 200 | $$\text{first $$ second}$$ 201 | 202 | $$$\text{first $$ second}$$ 203 | 204 | This is also display math, but (counterintuitively) it's allowed to be empty 205 | and expected to be as short as possible: 206 | 207 | $$$$\text{first $$ second}$$ 208 | 209 | Dollar signs must also be backslash-escaped if they occur within math: 210 | 211 | $\text{\$}$ 212 | 213 | $$x$x$$ 214 | 215 | ${$^$$ 216 | 217 | $}$$$$ 218 | 219 | $}$] $$ 220 | 221 | ## Edge case tests comparison with GitHub 222 | 223 | Test cases 224 | https://raw.githubusercontent.com/nschloe/github-math-bugs/db938ff690ab7c534d8195fe4a1a5163c20b1134/README.md 225 | 226 | Inline math wrapped in quotes 227 | 228 | $x$ $`y`$ 229 | 230 | Inline and display math in the same list 231 | 232 | - $a$ 233 | 234 | ```math 235 | a 236 | ``` 237 | 238 | $$ 239 | a 240 | $$ 241 | 242 | - ```math 243 | b 244 | ``` 245 | 246 | $$ 247 | b 248 | $$ 249 | 250 | Images and math in the same list 251 | 252 | - ![node logo](https://nodejs.org/static/images/logo.svg) 253 | - $x$ 254 | 255 | Inline and display math in `
` 256 | 257 |
258 | 259 | $A = 5$ 260 | 261 | $$ 262 | A = 5 263 | $$ 264 | 265 |
266 | 267 | `<` without surrounding whitespace 268 | 269 | $a c$ 312 | 313 | $[(a+b)c](d+e)$ 314 | 315 | ${a}_b c_{d}$ 316 | 317 | Dollar-math with spaces 318 | 319 | When $a \ne 0$, there are two solutions to $(ax^2 + bx + c = 0)$ and they are 320 | $$ x = {-b \pm \sqrt{b^2-4ac} \over 2a} $$ 321 | 322 | Spacing around dollar sign in math mode 323 | 324 | $x = \$$ 325 | 326 | Math in italic text 327 | 328 | _Equation $\Omega(69)$ in italic text_ 329 | 330 | Inline math can't be preceded by brackets, quotation marks etc. 331 | 332 | $\pi$ 333 | '$\pi$ 334 | "$\pi$ 335 | ($\pi$ 336 | [$\pi$ 337 | {$\pi$ 338 | /$\pi$ 339 | 340 | ## Relationship with tables 341 | 342 | As a block element, tables parsing is stronger than math. 343 | 344 | | first $|$ second | 345 | |--------|---------| 346 | | a ${ | }$ b | 347 | 348 | As a special case, pipes in math environments in tables are escaped 349 | with backslashes. Though backslash-escaped characters in math 350 | environments are normally passed through verbatim to the LaTeX 351 | engine, escaped pipes in tables are an exception like they 352 | are in code spans. 353 | 354 | The behavior of the table parser should be as-if it found the bounds 355 | of the table cell in a separate pass that only looked for the 356 | strings `|` and `\|`, treating pipes as boundaries and removing the 357 | escaping backslash before passing the string to the inline parser. 358 | 359 | | first $\|$ second | 360 | |-------------------| 361 | | a ${ \| }$ b | 362 | 363 | | Description | Test case | 364 | |-------------|-----------| 365 | | Single | $\$ | 366 | | Double | $\\$ | 367 | | Basic test | $\|$ | 368 | | Basic test 2| $\|\|\$ | 369 | | Basic test 3| $x\|y\|z\$| 370 | | Not pipe | $\.$ | 371 | | Combo | $\.\|$ | 372 | | Combo 2 | $\.\|\$ | 373 | | Extra | $\\\.$ | 374 | | Wait, what? | $\\|$ | 375 | | Wait, what? | $\\\|$ | 376 | | Wait, what? | $\\\\|$ | 377 | | Wait, what? | $\\\\\|$ | 378 | 379 | ## Implementation limits 380 | 381 | Implementations may impose limits on brace nesting to avoid performance issues, 382 | but at least three levels of nesting should be supported. 383 | 384 | Pulldown-cmark imposes the following limits: 385 | 386 | 1. At 25 levels of nesting, it switches from tracking nested pairs to simply 387 | counting the number of braces. This means the below example will spurriously 388 | recognize a math environment with the correct number of braces, but not 389 | nested correctly. 390 | 391 | This is not an inline math environment: $}{$ 392 | But, because it's nested too deeply, this is parsed as an inline math environment: 393 | {{{{{{{{{{{{{{{{{{{{{{{{{{{{{{ 394 | improperly $}{$ nested 395 | }}}}}}}}}}}}}}}}}}}}}}}}}}}}}} 396 | But this still isn't, because the braces are still counted: $}{$ 397 | 398 | This is also deeply nested, but, unlike the first example, 399 | they don't have an equal number of close braces and open braces, 400 | so aren't detected as math. 401 | {{{{{{{{{{{{{{{{{{{{{{{{{{{{{{ 402 | improperly $}$ nested ${$ example 403 | }}}}}}}}}}}}}}}}}}}}}}}}}}}}}} 404 | This, however, is detected ${}$ 405 | 406 | ${{{{{{{{{{{{{{{{{{{{{{{{{{{{{{ 407 | another improperly nested example 408 | }}}}}}}}}}}}}}}}}}}}}}}}}}}}}}$ 409 | 410 | 2. At 255 distinct brace-delimited groups, the counter rolls over. This means 411 | the below example will spurriously recognize an incorrectly-nested 412 | inline math environment. 413 | 414 | ${}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 20 brace pairs 415 | {}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 40 brace pairs 416 | {}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 60 brace pairs 417 | {}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 80 brace pairs 418 | {}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 100 brace pairs 419 | {}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 120 brace pairs 420 | {}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 140 brace pairs 421 | {}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 160 brace pairs 422 | {}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 180 brace pairs 423 | {}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 200 brace pairs 424 | {}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 220 brace pairs 425 | {}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 240 brace pairs 426 | {}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{$ 255 brace pairs and one unclosed brace 427 | 428 | 3. Thanks to rule 1, though, deeply-nested structures won't chew through all of 429 | the ID space. This means that the below example, even though it nests 255 430 | levels deep, parses correctly anyway. 431 | 432 | ${{{{{{{{{{{{{{{{{{{{ 20 open braces 433 | {{{{{{{{{{{{{{{{{{{{ 40 open braces 434 | {{{{{{{{{{{{{{{{{{{{ 60 open braces 435 | {{{{{{{{{{{{{{{{{{{{ 80 open braces 436 | {{{{{{{{{{{{{{{{{{{{ 100 open braces 437 | {{{{{{{{{{{{{{{{{{{{ 110 open braces 438 | {{{{{{{{{{{{{{{{{{{{ 120 open braces 439 | {{{{{{{{{{{{{{{{{{{{ 140 open braces 440 | {{{{{{{{{{{{{{{{{{{{ 160 open braces 441 | {{{{{{{{{{{{{{{{{{{{ 180 open braces 442 | {{{{{{{{{{{{{{{{{{{{ 200 open braces 443 | {{{{{{{{{{{{{{{{{{{{ 220 open braces 444 | {{{{{{{{{{{{{{{{{{{{ 240 open braces 445 | {{{{{{{{{{{{{{{ 255 open braces 446 | }}}}}}}}}}}}}}}}}}}} 20 close braces 447 | }}}}}}}}}}}}}}}}}}}} 40 close braces 448 | }}}}}}}}}}}}}}}}}}}} 60 close braces 449 | }}}}}}}}}}}}}}}}}}}} 80 close braces 450 | }}}}}}}}}}}}}}}}}}}} 100 close braces 451 | }}}}}}}}}}}}}}}}}}}} 120 close braces 452 | }}}}}}}}}}}}}}}}}}}} 140 close braces 453 | }}}}}}}}}}}}}}}}}}}} 160 close braces 454 | }}}}}}}}}}}}}}}}}}}} 180 close braces 455 | }}}}}}}}}}}}}}}}}}}} 200 close braces 456 | }}}}}}}}}}}}}}}}}}}} 220 close braces 457 | }}}}}}}}}}}}}}}}}}}} 240 close braces 458 | }}}}}}}}}}}}}}}{$ 255 close braces and one open brace 459 | 460 | [^a]: Lorem $a$ 461 | -------------------------------------------------------------------------------- /tests/fixtures/nested.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | *emphasized* 4 | 5 |
6 | 7 |
8 | second line 9 |
10 | 11 | inline html element 12 | -------------------------------------------------------------------------------- /tests/fixtures/ordered.md: -------------------------------------------------------------------------------- 1 | Ordered lists: 2 | 3 | 1. Lorem impsum 4 | 1. Nested 5 | 2. Inline 6 | 3. Text 7 | 2. dolor sit amet 8 | 1. Nested 9 | 10 | 2. With 11 | 12 | Paragraphs and nested blocks: 13 | 14 | > A quote 15 | 16 | And some text at the end 17 | 3. consetetur sadipscing elitr -------------------------------------------------------------------------------- /tests/fixtures/repeated-shortcut-links.md: -------------------------------------------------------------------------------- 1 | * [foo] 2 | * [foo] 3 | * [foo] 4 | 5 | [foo]: foo 6 | -------------------------------------------------------------------------------- /tests/fixtures/snapshots/stupicat-definition-list-output: -------------------------------------------------------------------------------- 1 | A paragraph 2 | 3 | 4 | A Term 5 | : Defined by block content 6 | 7 | * an embedded list entry 8 | * another entry 9 | 10 | 11 | 12 | Another Term 13 | : This one even has an embedded definition list 14 | 15 | 16 | A sub-term 17 | : It's definition 18 | 19 | Another sub-term 20 | : With a definition 21 | 22 | 23 | 24 | Back to a normal paragraph. 25 | 26 | --- 27 | 28 | > 29 | > A quoted paragraph 30 | > 31 | > 32 | > A quoted term 33 | > : its definition 34 | > 35 | > Another quoted term 36 | > : and its definition 37 | > -------------------------------------------------------------------------------- /tests/fixtures/snapshots/stupicat-event-by-event-output: -------------------------------------------------------------------------------- 1 | # CommonMark sample document 2 | 3 | ## Basic inline formatting 4 | 5 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam **nonumy 6 | eirmod tempor invidunt** ut labore et *dolore magna aliquyam erat*, sed diam 7 | voluptua. `At vero eos et` accusam et 8 | 9 | ## Headers 10 | 11 | ### Level 3 12 | 13 | #### Level 4 14 | 15 | ##### Level 5 16 | 17 | ###### Level 6 18 | 19 | ## [Links] 20 | 21 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod 22 | tempor invidunt ut labore et dolore magna aliquyam erat 23 | (), sed [`diam`] [`voluptua`]. 24 | 25 | Lorem ipsum dolor sit amet, [consetetur 26 | sadipscing](http://www.example.com/inline) elitr, sed diam nonumy eirmod tempor 27 | invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos 28 | et accusam et [justo duo dolores][1] et ea rebum. Stet clita kasd gubergren, no 29 | sea [takimata sanctus](./showcase.md) est Lorem ipsum dolor sit amet. 30 | 31 | * [bacon](<(>) 32 | * [bacon](<)>) 33 | * [bacon](test()) 34 | 35 | Ask to . 36 | 37 | ## [Images] 38 | 39 | Images as blocks: 40 | 41 | ![The Rust logo](./rust-logo-128x128.png) 42 | 43 | ## Lists 44 | 45 | Unordered lists: 46 | 47 | * Lorem impsum 48 | * Nested 49 | * Inline 50 | * Text 51 | * dolor sit amet 52 | * Nested 53 | 54 | * With Paragraphs and nested blocks: 55 | 56 | > 57 | > A quote 58 | 59 | And some text at the end 60 | 61 | * consetetur sadipscing elitr 62 | 63 | Ordered lists: 64 | 65 | 1. Lorem impsum 66 | 1. Nested 67 | 1. Inline 68 | 1. Text 69 | 1. dolor sit amet 70 | 1. Nested 71 | 72 | 1. With 73 | Paragraphs and nested blocks: 74 | 75 | > 76 | > A quote 77 | 78 | And some text at the end 79 | 80 | 1. consetetur sadipscing elitr 81 | 82 | And a mix of both: 83 | 84 | * Lorem impsum 85 | 1. Nested 86 | 1. Inline 87 | * With 88 | * Some 89 | * Nested 90 | * Bullets 91 | 1. Text 92 | * dolor sit amet 93 | 94 | ## Block level elements 95 | 96 | Block quotes 97 | 98 | > 99 | > Lorem ipsum dolor sit amet, *consetetur sadipscing elitr*, sed diam nonumy 100 | > eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam 101 | > voluptua. 102 | > 103 | > Lorem ipsum dolor sit amet, **consetetur sadipscing elitr**, sed diam nonumy 104 | > eirmod tempor invidunt ut `labore et dolore magna` aliquyam erat, sed diam 105 | > voluptua. 106 | 107 | > [!NOTE] 108 | > Highlights information that users should take into account, even when skimming. 109 | 110 | > [!TIP] 111 | > Optional information to help a user be more successful. 112 | 113 | > [!IMPORTANT] 114 | > Crucial information necessary for users to succeed. 115 | 116 | > [!WARNING] 117 | > Critical content demanding immediate user attention due to potential risks. 118 | 119 | > [!CAUTION] 120 | > Negative potential consequences of an action. 121 | 122 | Before we continue, have a ruler: 123 | 124 | --- 125 | 126 | Code blocks without syntax highlighting: 127 | 128 | ```` 129 | Some plain 130 | code block 131 | fooo 132 | ```` 133 | 134 | Or with syntax highlighting, eg, Rust: 135 | 136 | ````rust 137 | fn main() { 138 | println!("Hello world") 139 | } 140 | ```` 141 | 142 | Or Haskell: 143 | 144 | ````haskell 145 | main :: IO () 146 | main = putStrLn "Hello World" 147 | ```` 148 | 149 | Or Scala: 150 | 151 | ````scala 152 | object HelloWorld { 153 | def main(args: Array[String]): Unit = { 154 | println("Hello, world!") 155 | } 156 | } 157 | ```` 158 | 159 | Or raw codeblocks: 160 | 161 | ```` 162 | ```bash 163 | echo 'hi from the innner codeblock' 164 | ``` 165 | ```` 166 | 167 | ## HTML 168 | 169 | We can have block html: 170 | 171 |
172 |

173 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod 174 | tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At 175 | vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, 176 | no sea takimata sanctus est Lorem ipsum dolor sit amet. 177 |

178 |
179 | 180 | Or inline HTML, as in this paragraph: Lorem ipsum dolor sit amet, consetetur 181 | sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore 182 | et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et 183 | justo duo dolores et ea rebum. Stet clita kasd gubergren, no 184 | sea takimata sanctus est Lorem ipsum dolor sit amet. 185 | 186 | ## Math 187 | 188 | This sentence uses `$` delimiters to show math inline: $\sqrt{3x-1}+(1+x)^2$ 189 | 190 | This sentence uses $\` and \`$ delimiters to show math inline: $`\sqrt{3x-1}+(1+x)^2`$ 191 | 192 | **The Cauchy-Schwarz Inequality** 193 | $$\left( \sum_{k=1}^n a_k b_k \right)^2 \leq \left( \sum_{k=1}^n a_k^2 \right) \left( \sum_{k=1}^n b_k^2 \right)$$ 194 | 195 | **The Cauchy-Schwarz Inequality** 196 | 197 | ````math 198 | \left( \sum_{k=1}^n a_k b_k \right)^2 \leq \left( \sum_{k=1}^n a_k^2 \right) \left( \sum_{k=1}^n b_k^2 \right) 199 | ```` 200 | 201 | This expression uses `\$` to display a dollar sign: $`\sqrt{\$4}`$ 202 | 203 | To split $100 in half, we calculate $100/2$ 204 | 205 | ## Escapes 206 | 207 | We now support escaping special characters, such as in \_\_init\_\_.py and in formulas 208 | like \\( \int x dx = \frac{x^2}{2} + C \\). 209 | 210 | What about \*this\* or \*\*that\*\*? 211 | 212 | \# heading? 213 | 214 | \[disabled inline link\](target) 215 | 216 | \[disabled named link\]: target 217 | 218 | [named-link][enabled] 219 | 220 | \[disabled named link\]\[disabled\] 221 | 222 | \|table| 223 | \|-----| 224 | 225 | \`code\` 226 | 227 | \] a closing bracket does nothing 228 | 229 | \ 230 | 231 | \> inline code blocks 232 | 233 | In code blocks, there will be no escaping: 234 | 235 | ```` 236 | A raw block inside 237 | ``` 238 | foo 239 | ``` 240 | 241 | * | < > # 242 | ```` 243 | 244 | ### Entity escapes 245 | 246 | © 2280 one world government, inc 247 | 248 | paragraph starts with a literal tab. 249 | 250 | I'm using markdown like a typewriter, 251 | probably because this document started life in a WYSIWYG 252 | editor and got converted... 253 | 254 | [Links]: http://www.example.com/shortcut 255 | [`diam`]: http://www.example.com/shortcut_code_diam 256 | [`voluptua`]: http://www.example.com/shortcut_code_voluptua 257 | [1]: http://www.example.com/reference 258 | [Images]: http://www.example.com/another_shortcut 259 | [enabled]: bah -------------------------------------------------------------------------------- /tests/fixtures/snapshots/stupicat-heading-id-classes-output: -------------------------------------------------------------------------------- 1 | # nothing 2 | 3 | # with ID { #myh1 } 4 | 5 | ## with class { .classh2 } 6 | 7 | ### multiple { #myh3 .classh3 } 8 | 9 | # text { #id .class1 .class2 myattr, other_attr=myvalue } -------------------------------------------------------------------------------- /tests/fixtures/snapshots/stupicat-indented-code-block: -------------------------------------------------------------------------------- 1 | codeblock: 2 | 3 | fn main() { 4 | println!("Hello, world!"); 5 | } 6 | -------------------------------------------------------------------------------- /tests/fixtures/snapshots/stupicat-lists-nested-output: -------------------------------------------------------------------------------- 1 | 1. list paragraph 1 2 | ```` 3 | code sample 4 | ```` 5 | 6 | 1. list paragraph 2 7 | 8 | --- 9 | 10 | 1. list paragraph 1 11 | 12 | ```` 13 | code sample 14 | ```` 15 | 16 | 1. list paragraph 2 17 | 18 | --- 19 | 20 | 1. list paragraph 1 21 | 22 | code sample 23 | 24 | 25 | 1. list paragraph 2 -------------------------------------------------------------------------------- /tests/fixtures/snapshots/stupicat-math-output: -------------------------------------------------------------------------------- 1 | 2 | 3 | # `$`-delimited LaTeX Math in pulldown-cmark 4 | 5 | Mathematical expressions extension. Syntax based on 6 | . 7 | 8 | Inline mode mathematical expressions: 9 | 10 | This sentence uses `$` delimiters to show math inline: $\sqrt{3x-1}+(1+x)^2$ 11 | $\sum_{k=1}^n a_k b_k$: Mathematical expression at head of line 12 | 13 | Display mode mathematical expressions: 14 | 15 | **The Cauchy-Schwarz Inequality** 16 | 17 | $$\left( \sum_{k=1}^n a_k b_k \right)^2 \leq \left( \sum_{k=1}^n a_k^2 \right) \left( \sum_{k=1}^n b_k^2 \right)$$ 18 | 19 | Inline math expressions cannot be empty, but display mode expressions can. 20 | 21 | Oops empty $$ expression. 22 | 23 | $$$$ 24 | 25 | This is a greedy, left-to-right parser. 26 | 27 | $x$$$$$$$y$$ 28 | 29 | $x$$$$$$y$$ 30 | 31 | $$x$$$$$$y$$ 32 | 33 | Math expressions pass their content through as-is, ignoring any other inline 34 | Markdown constructs: 35 | 36 | $ac$ 37 | 38 | $${a*b*c} _c_ d$$ 39 | 40 | $not `code`$ 41 | 42 | $![not an](/image)$ 43 | 44 | $$ 45 | 46 | $α$ 47 | 48 | Sole `$` characters without a matching pair in the same block element 49 | are handled as normal text. 50 | 51 | Hello $world. 52 | 53 | Dollar at end of line$ 54 | 55 | Mathematical expressions can continue across multiple lines: 56 | 57 | $5x + 2 = 58 | 17$ 59 | 60 | $$\left( \sum_{k=1}^n a_k b_k \right)^2 \leq \left( \sum_{k=1}^n a_k^2 \right) 61 | \left( \sum_{k=1}^n b_k^2 \right)$$ 62 | 63 | Markdown hard breaks are also not recognized inside math expressions: 64 | 65 | $not a\ 66 | hard break 67 | either$ 68 | 69 | `$` character can be escaped with backslash in mathematical expressions: 70 | 71 | $\$$ 72 | 73 | $$y = \$ x$$ 74 | 75 | Inline mode math expressions cannot contain unescaped `$` characters. 76 | Neither can display math. 77 | 78 | $x $ x$ 79 | 80 | $$ $ $$ 81 | 82 | alpha$$beta$gamma$$delta 83 | 84 | Inline math expressions cannot start or end with whitespace, including newlines: 85 | 86 | these are not math texts: $ y=x$, $y=x $, $ 87 | y=x$ and $y=x 88 | $ 89 | 90 | > 91 | > The start of a line counts as whitespace $2 + 92 | > $ 93 | 94 | While displays can start with whitespace, {${ 95 | they should not allow inlines to do that $$2 + 96 | $*$ 97 | 98 | Inline math expressions do not need to be surrounded with whitespace: 99 | 100 | these are math texts: foo$y=x$bar and $y=x$bar and foo$y=x$ bar 101 | 102 | Inline math expressions can be surrounded by punctuation: 103 | 104 | math texts: $x=y$! and $x=y$? and $x=y$: and $x=y$. and $x=y$" 105 | 106 | also math texts: !$x=y$! and ?$x=y$? and :$x=y$: and .$x=y$. and "$x=y$" 107 | 108 | braces: ($x=y$) \[$x=y$\] {$x=y$} 109 | 110 | Math expression as only item on a line: 111 | 112 | $x=y$ 113 | 114 | Math expressions can be immediately followed by other math expressions: 115 | 116 | $a$$b$ 117 | 118 | $a$$$b$$ 119 | 120 | $$a$$$b$ 121 | 122 | $$a$$$$b$$ 123 | 124 | Both inline and display mode math expressions are inline elements with the same 125 | precedence as code spans. The leftmost valid element takes priority: 126 | 127 | $Inline `first$ then\` code 128 | 129 | `Code $first` then$ inline 130 | 131 | $$ Display `first $$ then\` code 132 | 133 | `Code $$ first` then $$ display 134 | 135 | Indicators of block structure take precedence over math expressions: 136 | 137 | $x + y - z$ 138 | 139 | $x + y 140 | 141 | * z$ 142 | 143 | $$ x + y 144 | 145 | > 146 | > z $$ 147 | 148 | This also means that math expressions cannot contain empty lines, since they 149 | start a new paragraph: 150 | 151 | $not 152 | 153 | math$ 154 | 155 | $$ 156 | not 157 | 158 | math 159 | $$ 160 | 161 | It also implies that math notation has lower 162 | parsing power than block elements. 163 | 164 | * $not 165 | * 166 | * 167 | math$ 168 | 169 | Note that math can contain embedded math. In scanning 170 | for a closing delimiter, we skip material in balanced 171 | curly braces: 172 | 173 | This is display math: 174 | $$ 175 | \text{Hello $x^2$} 176 | $$ 177 | And this is inline math: 178 | $\text{Hello $x$ there!}$ 179 | 180 | Math expressions must be nested within balanced curly braces. 181 | Backslash-escaped braces do not count. 182 | 183 | This is not valid math: $}{$ 184 | 185 | Neither is this: { $}{$ } 186 | 187 | This is: $\}\{$ 188 | 189 | This is: $\}$ 190 | 191 | Math environment contains 2+2: $}$2+2$ 192 | 193 | Math environment contains y: $x {$ $ } $y$ 194 | 195 | Math expressions must contain properly nested braces. 196 | 197 | This is not display math. It is inline math: 198 | 199 | $$\text{first $$ second}$ 200 | 201 | $$$\text{first $$ second}$ 202 | 203 | This is display math: 204 | 205 | $$\text{first $$ second}$$ 206 | 207 | $$$\text{first $$ second}$$ 208 | 209 | This is also display math, but (counterintuitively) it's allowed to be empty 210 | and expected to be as short as possible: 211 | 212 | $$$$\\text{first $$ second}$$ 213 | 214 | Dollar signs must also be backslash-escaped if they occur within math: 215 | 216 | $\text{\$}$ 217 | 218 | $$x$x$$ 219 | 220 | ${$^$$ 221 | 222 | $}$$$$ 223 | 224 | $}$\] $$ 225 | 226 | ## Edge case tests comparison with GitHub 227 | 228 | Test cases 229 | https://raw.githubusercontent.com/nschloe/github-math-bugs/db938ff690ab7c534d8195fe4a1a5163c20b1134/README.md 230 | 231 | Inline math wrapped in quotes 232 | 233 | $x$ $`y`$ 234 | 235 | Inline and display math in the same list 236 | 237 | * $a$ 238 | 239 | ````math 240 | a 241 | ```` 242 | 243 | $$ 244 | a 245 | $$ 246 | 247 | * 248 | ````math 249 | b 250 | ```` 251 | 252 | $$ 253 | b 254 | $$ 255 | 256 | Images and math in the same list 257 | 258 | * ![node logo](https://nodejs.org/static/images/logo.svg) 259 | * $x$ 260 | 261 | Inline and display math in `
` 262 | 263 |
264 | 265 | $A = 5$ 266 | 267 | $$ 268 | A = 5 269 | $$ 270 | 271 |
272 | 273 | `<` without surrounding whitespace 274 | 275 | $a c$ 318 | 319 | $[(a+b)c](d+e)$ 320 | 321 | ${a}_b c_{d}$ 322 | 323 | Dollar-math with spaces 324 | 325 | When $a \ne 0$, there are two solutions to $(ax^2 + bx + c = 0)$ and they are 326 | $$ x = {-b \pm \sqrt{b^2-4ac} \over 2a} $$ 327 | 328 | Spacing around dollar sign in math mode 329 | 330 | $x = \$$ 331 | 332 | Math in italic text 333 | 334 | *Equation $\Omega(69)$ in italic text* 335 | 336 | Inline math can't be preceded by brackets, quotation marks etc. 337 | 338 | $\pi$ 339 | '$\pi$ 340 | "$\pi$ 341 | ($\pi$ 342 | \[$\pi$ 343 | {$\pi$ 344 | /$\pi$ 345 | 346 | ## Relationship with tables 347 | 348 | As a block element, tables parsing is stronger than math. 349 | 350 | |first $|$ second| 351 | |-------|--------| 352 | |a ${|}$ b| 353 | 354 | As a special case, pipes in math environments in tables are escaped 355 | with backslashes. Though backslash-escaped characters in math 356 | environments are normally passed through verbatim to the LaTeX 357 | engine, escaped pipes in tables are an exception like they 358 | are in code spans. 359 | 360 | The behavior of the table parser should be as-if it found the bounds 361 | of the table cell in a separate pass that only looked for the 362 | strings `|` and `\|`, treating pipes as boundaries and removing the 363 | escaping backslash before passing the string to the inline parser. 364 | 365 | |first $|$ second| 366 | |-------------| 367 | |a ${ | }$ b| 368 | 369 | |Description|Test case| 370 | |-----------|---------| 371 | |Single|$$| 372 | |Double|$\\$| 373 | |Basic test|$|$| 374 | |Basic test 2|$\|\|$| 375 | |Basic test 3|$x\|y\|z$| 376 | |Not pipe|$\.$| 377 | |Combo|$\.|$| 378 | |Combo 2|$.\|$| 379 | |Extra|$\\\.$| 380 | |Wait, what?|$\|$| 381 | |Wait, what?|$\\|$| 382 | |Wait, what?|$\\\|$| 383 | |Wait, what?|$\\\\|$| 384 | 385 | ## Implementation limits 386 | 387 | Implementations may impose limits on brace nesting to avoid performance issues, 388 | but at least three levels of nesting should be supported. 389 | 390 | Pulldown-cmark imposes the following limits: 391 | 392 | 1. At 25 levels of nesting, it switches from tracking nested pairs to simply 393 | counting the number of braces. This means the below example will spurriously 394 | recognize a math environment with the correct number of braces, but not 395 | nested correctly. 396 | 397 | This is not an inline math environment: $}{$ 398 | But, because it's nested too deeply, this is parsed as an inline math environment: 399 | {{{{{{{{{{{{{{{{{{{{{{{{{{{{{{ 400 | improperly $}{$ nested 401 | }}}}}}}}}}}}}}}}}}}}}}}}}}}}}} 402 | But this still isn't, because the braces are still counted: $}{$ 403 | 404 | This is also deeply nested, but, unlike the first example, 405 | they don't have an equal number of close braces and open braces, 406 | so aren't detected as math. 407 | {{{{{{{{{{{{{{{{{{{{{{{{{{{{{{ 408 | improperly $}$ nested ${$ example 409 | }}}}}}}}}}}}}}}}}}}}}}}}}}}}}} 410 | This, however, is detected ${}$ 411 | 412 | ${{{{{{{{{{{{{{{{{{{{{{{{{{{{{{ 413 | another improperly nested example 414 | }}}}}}}}}}}}}}}}}}}}}}}}}}}}}}$ 415 | 416 | 2. At 255 distinct brace-delimited groups, the counter rolls over. This means 417 | the below example will spurriously recognize an incorrectly-nested 418 | inline math environment. 419 | 420 | ${}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 20 brace pairs 421 | {}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 40 brace pairs 422 | {}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 60 brace pairs 423 | {}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 80 brace pairs 424 | {}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 100 brace pairs 425 | {}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 120 brace pairs 426 | {}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 140 brace pairs 427 | {}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 160 brace pairs 428 | {}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 180 brace pairs 429 | {}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 200 brace pairs 430 | {}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 220 brace pairs 431 | {}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{} 240 brace pairs 432 | {}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{$ 255 brace pairs and one unclosed brace 433 | 434 | 3. Thanks to rule 1, though, deeply-nested structures won't chew through all of 435 | the ID space. This means that the below example, even though it nests 255 436 | levels deep, parses correctly anyway. 437 | 438 | ${{{{{{{{{{{{{{{{{{{{ 20 open braces 439 | {{{{{{{{{{{{{{{{{{{{ 40 open braces 440 | {{{{{{{{{{{{{{{{{{{{ 60 open braces 441 | {{{{{{{{{{{{{{{{{{{{ 80 open braces 442 | {{{{{{{{{{{{{{{{{{{{ 100 open braces 443 | {{{{{{{{{{{{{{{{{{{{ 110 open braces 444 | {{{{{{{{{{{{{{{{{{{{ 120 open braces 445 | {{{{{{{{{{{{{{{{{{{{ 140 open braces 446 | {{{{{{{{{{{{{{{{{{{{ 160 open braces 447 | {{{{{{{{{{{{{{{{{{{{ 180 open braces 448 | {{{{{{{{{{{{{{{{{{{{ 200 open braces 449 | {{{{{{{{{{{{{{{{{{{{ 220 open braces 450 | {{{{{{{{{{{{{{{{{{{{ 240 open braces 451 | {{{{{{{{{{{{{{{ 255 open braces 452 | }}}}}}}}}}}}}}}}}}}} 20 close braces 453 | }}}}}}}}}}}}}}}}}}}} 40 close braces 454 | }}}}}}}}}}}}}}}}}}}} 60 close braces 455 | }}}}}}}}}}}}}}}}}}}} 80 close braces 456 | }}}}}}}}}}}}}}}}}}}} 100 close braces 457 | }}}}}}}}}}}}}}}}}}}} 120 close braces 458 | }}}}}}}}}}}}}}}}}}}} 140 close braces 459 | }}}}}}}}}}}}}}}}}}}} 160 close braces 460 | }}}}}}}}}}}}}}}}}}}} 180 close braces 461 | }}}}}}}}}}}}}}}}}}}} 200 close braces 462 | }}}}}}}}}}}}}}}}}}}} 220 close braces 463 | }}}}}}}}}}}}}}}}}}}} 240 close braces 464 | }}}}}}}}}}}}}}}{$ 255 close braces and one open brace 465 | 466 | [^a]: Lorem $a$ -------------------------------------------------------------------------------- /tests/fixtures/snapshots/stupicat-nested-output: -------------------------------------------------------------------------------- 1 |
2 | 3 | *emphasized* 4 | 5 |
6 | 7 |
8 | second line 9 |
10 | 11 | inline html element -------------------------------------------------------------------------------- /tests/fixtures/snapshots/stupicat-ordered-output: -------------------------------------------------------------------------------- 1 | Ordered lists: 2 | 3 | 1. Lorem impsum 4 | 5 | 1. Nested 6 | 1. Inline 7 | 1. Text 8 | 1. dolor sit amet 9 | 10 | 1. Nested 11 | 12 | 1. With 13 | 14 | Paragraphs and nested blocks: 15 | 16 | > 17 | > A quote 18 | 19 | And some text at the end 20 | 21 | 1. consetetur sadipscing elitr -------------------------------------------------------------------------------- /tests/fixtures/snapshots/stupicat-output: -------------------------------------------------------------------------------- 1 | # CommonMark sample document 2 | 3 | ## Basic inline formatting 4 | 5 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam **nonumy 6 | eirmod tempor invidunt** ut labore et *dolore magna aliquyam erat*, sed diam 7 | voluptua. `At vero eos et` accusam et 8 | 9 | ## Headers 10 | 11 | ### Level 3 12 | 13 | #### Level 4 14 | 15 | ##### Level 5 16 | 17 | ###### Level 6 18 | 19 | ## [Links] 20 | 21 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod 22 | tempor invidunt ut labore et dolore magna aliquyam erat 23 | (), sed [`diam`] [`voluptua`]. 24 | 25 | Lorem ipsum dolor sit amet, [consetetur 26 | sadipscing](http://www.example.com/inline) elitr, sed diam nonumy eirmod tempor 27 | invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos 28 | et accusam et [justo duo dolores][1] et ea rebum. Stet clita kasd gubergren, no 29 | sea [takimata sanctus](./showcase.md) est Lorem ipsum dolor sit amet. 30 | 31 | * [bacon](<(>) 32 | * [bacon](<)>) 33 | * [bacon](test()) 34 | 35 | Ask to . 36 | 37 | ## [Images] 38 | 39 | Images as blocks: 40 | 41 | ![The Rust logo](./rust-logo-128x128.png) 42 | 43 | ## Lists 44 | 45 | Unordered lists: 46 | 47 | * Lorem impsum 48 | * Nested 49 | * Inline 50 | * Text 51 | * dolor sit amet 52 | * Nested 53 | 54 | * With Paragraphs and nested blocks: 55 | 56 | > 57 | > A quote 58 | 59 | And some text at the end 60 | 61 | * consetetur sadipscing elitr 62 | 63 | Ordered lists: 64 | 65 | 1. Lorem impsum 66 | 1. Nested 67 | 1. Inline 68 | 1. Text 69 | 1. dolor sit amet 70 | 1. Nested 71 | 72 | 1. With 73 | Paragraphs and nested blocks: 74 | 75 | > 76 | > A quote 77 | 78 | And some text at the end 79 | 80 | 1. consetetur sadipscing elitr 81 | 82 | And a mix of both: 83 | 84 | * Lorem impsum 85 | 1. Nested 86 | 1. Inline 87 | * With 88 | * Some 89 | * Nested 90 | * Bullets 91 | 1. Text 92 | * dolor sit amet 93 | 94 | ## Block level elements 95 | 96 | Block quotes 97 | 98 | > 99 | > Lorem ipsum dolor sit amet, *consetetur sadipscing elitr*, sed diam nonumy 100 | > eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam 101 | > voluptua. 102 | > 103 | > Lorem ipsum dolor sit amet, **consetetur sadipscing elitr**, sed diam nonumy 104 | > eirmod tempor invidunt ut `labore et dolore magna` aliquyam erat, sed diam 105 | > voluptua. 106 | 107 | > [!NOTE] 108 | > Highlights information that users should take into account, even when skimming. 109 | 110 | > [!TIP] 111 | > Optional information to help a user be more successful. 112 | 113 | > [!IMPORTANT] 114 | > Crucial information necessary for users to succeed. 115 | 116 | > [!WARNING] 117 | > Critical content demanding immediate user attention due to potential risks. 118 | 119 | > [!CAUTION] 120 | > Negative potential consequences of an action. 121 | 122 | Before we continue, have a ruler: 123 | 124 | --- 125 | 126 | Code blocks without syntax highlighting: 127 | 128 | ```` 129 | Some plain 130 | code block 131 | fooo 132 | ```` 133 | 134 | Or with syntax highlighting, eg, Rust: 135 | 136 | ````rust 137 | fn main() { 138 | println!("Hello world") 139 | } 140 | ```` 141 | 142 | Or Haskell: 143 | 144 | ````haskell 145 | main :: IO () 146 | main = putStrLn "Hello World" 147 | ```` 148 | 149 | Or Scala: 150 | 151 | ````scala 152 | object HelloWorld { 153 | def main(args: Array[String]): Unit = { 154 | println("Hello, world!") 155 | } 156 | } 157 | ```` 158 | 159 | Or raw codeblocks: 160 | 161 | ```` 162 | ```bash 163 | echo 'hi from the innner codeblock' 164 | ``` 165 | ```` 166 | 167 | ## HTML 168 | 169 | We can have block html: 170 | 171 |
172 |

173 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod 174 | tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At 175 | vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, 176 | no sea takimata sanctus est Lorem ipsum dolor sit amet. 177 |

178 |
179 | 180 | Or inline HTML, as in this paragraph: Lorem ipsum dolor sit amet, consetetur 181 | sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore 182 | et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et 183 | justo duo dolores et ea rebum. Stet clita kasd gubergren, no 184 | sea takimata sanctus est Lorem ipsum dolor sit amet. 185 | 186 | ## Math 187 | 188 | This sentence uses `$` delimiters to show math inline: $\sqrt{3x-1}+(1+x)^2$ 189 | 190 | This sentence uses $\` and \`$ delimiters to show math inline: $`\sqrt{3x-1}+(1+x)^2`$ 191 | 192 | **The Cauchy-Schwarz Inequality** 193 | $$\left( \sum_{k=1}^n a_k b_k \right)^2 \leq \left( \sum_{k=1}^n a_k^2 \right) \left( \sum_{k=1}^n b_k^2 \right)$$ 194 | 195 | **The Cauchy-Schwarz Inequality** 196 | 197 | ````math 198 | \left( \sum_{k=1}^n a_k b_k \right)^2 \leq \left( \sum_{k=1}^n a_k^2 \right) \left( \sum_{k=1}^n b_k^2 \right) 199 | ```` 200 | 201 | This expression uses `\$` to display a dollar sign: $`\sqrt{\$4}`$ 202 | 203 | To split $100 in half, we calculate $100/2$ 204 | 205 | ## Escapes 206 | 207 | We now support escaping special characters, such as in \_\_init\_\_.py and in formulas 208 | like \\( \int x dx = \frac{x^2}{2} + C \\). 209 | 210 | What about \*this\* or \*\*that\*\*? 211 | 212 | \# heading? 213 | 214 | \[disabled inline link\](target) 215 | 216 | \[disabled named link\]: target 217 | 218 | [named-link][enabled] 219 | 220 | \[disabled named link\]\[disabled\] 221 | 222 | \|table| 223 | \|-----| 224 | 225 | \`code\` 226 | 227 | \] a closing bracket does nothing 228 | 229 | \ 230 | 231 | \> inline code blocks 232 | 233 | In code blocks, there will be no escaping: 234 | 235 | ```` 236 | A raw block inside 237 | ``` 238 | foo 239 | ``` 240 | 241 | * | < > # 242 | ```` 243 | 244 | ### Entity escapes 245 | 246 | © 2280 one world government, inc 247 | 248 | paragraph starts with a literal tab. 249 | 250 | I'm using markdown like a typewriter, 251 | probably because this document started life in a WYSIWYG 252 | editor and got converted... 253 | 254 | [Links]: http://www.example.com/shortcut 255 | [`diam`]: http://www.example.com/shortcut_code_diam 256 | [`voluptua`]: http://www.example.com/shortcut_code_voluptua 257 | [1]: http://www.example.com/reference 258 | [Images]: http://www.example.com/another_shortcut 259 | [enabled]: bah -------------------------------------------------------------------------------- /tests/fixtures/snapshots/stupicat-repeated-shortcut-links-output: -------------------------------------------------------------------------------- 1 | * [foo] 2 | * [foo] 3 | * [foo] 4 | 5 | [foo]: foo -------------------------------------------------------------------------------- /tests/fixtures/snapshots/stupicat-super-sub-script-output: -------------------------------------------------------------------------------- 1 | this is an example of superscript. 2 | 3 | this is an example of subscript. 4 | 5 | 1st of the month. -------------------------------------------------------------------------------- /tests/fixtures/snapshots/stupicat-table-output: -------------------------------------------------------------------------------- 1 | Colons can be used to align columns. 2 | 3 | |Tables|Are|Cool|yo| 4 | |------|:-:|---:|:-| 5 | |col 3 is|right-aligned|$1600|x| 6 | |col 2 is|centered|$12|y| 7 | |zebra stripes|are neat|$1|z| 8 | 9 | There must be at least 3 dashes separating each header cell. 10 | The outer pipes (|) are optional, and you don't need to make the 11 | raw Markdown line up prettily. You can also use inline Markdown. 12 | 13 | > 14 | > |Markdown|Less|Pretty| 15 | > |--------|----|------| 16 | > |*Still*|`renders`|**nicely**| 17 | > |1|2|3| 18 | 19 | |Target|std|rustc|cargo|notes| 20 | |------|---|-----|-----|-----| 21 | |`x86_64-unknown-linux-musl`|✓|||64-bit Linux with MUSL| 22 | |`arm-linux-androideabi`|✓|||ARM Android| 23 | |`arm-unknown-linux-gnueabi`|✓|✓||ARM Linux (2.6.18+)| 24 | |`arm-unknown-linux-gnueabihf`|✓|✓||ARM Linux (2.6.18+)| 25 | |`aarch64-unknown-linux-gnu`|✓|||ARM64 Linux (2.6.18+)| 26 | |`mips-unknown-linux-gnu`|✓|||MIPS Linux (2.6.18+)| 27 | |`mipsel-unknown-linux-gnu`|✓|||MIPS (LE) Linux (2.6.18+)| 28 | 29 | Tables with formatting in header 30 | 31 | |header|`code`|*emphasize*|*strong*| 32 | |------|------|---------|------| 33 | |data 1|data 2|data 3|data 4| -------------------------------------------------------------------------------- /tests/fixtures/snapshots/stupicat-table-with-escapes-output: -------------------------------------------------------------------------------- 1 | |First|Second|Third|Fourth| 2 | |-----|------|-----|------| 3 | |`\|`|`\\`|`a\[b\]`|`>>`| 4 | 5 | Normal inline code with pipes `|` should not be re-escaped, because it does not 6 | cause a problem! -------------------------------------------------------------------------------- /tests/fixtures/snapshots/stupicat-table-with-html-output: -------------------------------------------------------------------------------- 1 | # Heading 2 | 3 | |Head 1|Head 2| 4 | |------|------| 5 | |Row 1|Row 2| 6 | 7 | Paragraph -------------------------------------------------------------------------------- /tests/fixtures/snapshots/stupicat-toml-frontmatter-output: -------------------------------------------------------------------------------- 1 | +++ 2 | key = value 3 | +++ 4 | 5 | # Frontmatter should be supported -------------------------------------------------------------------------------- /tests/fixtures/snapshots/stupicat-unordered-output: -------------------------------------------------------------------------------- 1 | Unordered lists: 2 | 3 | * Lorem impsum 4 | * Nested 5 | * Inline 6 | * Text 7 | * dolor sit amet 8 | * Nested 9 | 10 | * With 11 | 12 | Paragraphs and nested blocks: 13 | 14 | > 15 | > A quote 16 | 17 | And some text at the end -------------------------------------------------------------------------------- /tests/fixtures/snapshots/stupicat-yaml-frontmatter-output: -------------------------------------------------------------------------------- 1 | --- 2 | key: value 3 | --- 4 | 5 | # Frontmatter should be supported -------------------------------------------------------------------------------- /tests/fixtures/super-sub-script.md: -------------------------------------------------------------------------------- 1 | this is an example of superscript. 2 | 3 | this is an example of subscript. 4 | 5 | 1st of the month. 6 | -------------------------------------------------------------------------------- /tests/fixtures/table-with-escapes.md: -------------------------------------------------------------------------------- 1 | | First | Second | Third | Fourth | 2 | | ----- | ------ | -------- | ------ | 3 | | `\|` | `\\` | `a\[b\]` | `>>` | 4 | 5 | Normal inline code with pipes `|` should not be re-escaped, because it does not 6 | cause a problem! 7 | -------------------------------------------------------------------------------- /tests/fixtures/table-with-html.md: -------------------------------------------------------------------------------- 1 | # Heading 2 | 3 | | Head 1 | Head 2 | 4 | |--------|--------| 5 | | Row 1 | Row 2 | 6 | 7 | Paragraph 8 | -------------------------------------------------------------------------------- /tests/fixtures/table.md: -------------------------------------------------------------------------------- 1 | Colons can be used to align columns. 2 | 3 | | Tables | Are | Cool | yo | 4 | |---------------|:-------------:|------:|:---| 5 | | col 3 is | right-aligned | $1600 | x | 6 | | col 2 is | centered | $12 | y | 7 | | zebra stripes | are neat | $1 | z | 8 | 9 | There must be at least 3 dashes separating each header cell. 10 | The outer pipes (|) are optional, and you don't need to make the 11 | raw Markdown line up prettily. You can also use inline Markdown. 12 | 13 | > Markdown | Less | Pretty 14 | > --- | --- | --- 15 | > *Still* | `renders` | **nicely** 16 | > 1 | 2 | 3 17 | 18 | | Target | std |rustc|cargo| notes | 19 | |-------------------------------|-----|-----|-----|----------------------------| 20 | | `x86_64-unknown-linux-musl` | ✓ | | | 64-bit Linux with MUSL | 21 | | `arm-linux-androideabi` | ✓ | | | ARM Android | 22 | | `arm-unknown-linux-gnueabi` | ✓ | ✓ | | ARM Linux (2.6.18+) | 23 | | `arm-unknown-linux-gnueabihf` | ✓ | ✓ | | ARM Linux (2.6.18+) | 24 | | `aarch64-unknown-linux-gnu` | ✓ | | | ARM64 Linux (2.6.18+) | 25 | | `mips-unknown-linux-gnu` | ✓ | | | MIPS Linux (2.6.18+) | 26 | | `mipsel-unknown-linux-gnu` | ✓ | | | MIPS (LE) Linux (2.6.18+) | 27 | 28 | Tables with formatting in header 29 | 30 | | header | `code` | _emphasize_ | *strong* | 31 | | ------ | ------ | ----------- | -------- | 32 | | data 1 | data 2 | data 3 | data 4 | 33 | -------------------------------------------------------------------------------- /tests/fixtures/toml-frontmatter.md: -------------------------------------------------------------------------------- 1 | +++ 2 | key = value 3 | +++ 4 | 5 | # Frontmatter should be supported 6 | -------------------------------------------------------------------------------- /tests/fixtures/unordered.md: -------------------------------------------------------------------------------- 1 | Unordered lists: 2 | 3 | * Lorem impsum 4 | * Nested 5 | * Inline 6 | * Text 7 | * dolor sit amet 8 | * Nested 9 | 10 | * With 11 | 12 | Paragraphs and nested blocks: 13 | 14 | > A quote 15 | 16 | And some text at the end 17 | -------------------------------------------------------------------------------- /tests/fixtures/yaml-frontmatter.md: -------------------------------------------------------------------------------- 1 | --- 2 | key: value 3 | --- 4 | 5 | # Frontmatter should be supported 6 | -------------------------------------------------------------------------------- /tests/integrate/display.rs: -------------------------------------------------------------------------------- 1 | use pulldown_cmark::Event; 2 | use pulldown_cmark_to_cmark::*; 3 | 4 | fn s(e: Event) -> String { 5 | es([e]) 6 | } 7 | fn es<'a>(es: impl IntoIterator>) -> String { 8 | let mut buf = String::new(); 9 | cmark(es.into_iter(), &mut buf).unwrap(); 10 | buf 11 | } 12 | mod code { 13 | use pulldown_cmark::Event::*; 14 | 15 | use super::s; 16 | 17 | #[test] 18 | fn code() { 19 | assert_eq!(s(Code("foo\nbar".into())), "`foo\nbar`"); 20 | } 21 | } 22 | 23 | mod rule { 24 | use pulldown_cmark::Event::*; 25 | 26 | use super::s; 27 | 28 | #[test] 29 | fn rule() { 30 | assert_eq!(s(Rule), "---"); 31 | } 32 | } 33 | 34 | mod start { 35 | use pulldown_cmark::{ 36 | Alignment::{self, Center, Left, Right}, 37 | BlockQuoteKind, CodeBlockKind, 38 | Event::*, 39 | HeadingLevel, 40 | LinkType::*, 41 | Tag::*, 42 | }; 43 | 44 | use super::{es, s}; 45 | 46 | #[test] 47 | fn paragraph() { 48 | assert_eq!(s(Start(Paragraph)), ""); 49 | } 50 | #[test] 51 | fn header1() { 52 | assert_eq!( 53 | s(Start(Heading { 54 | level: HeadingLevel::H1, 55 | id: None, 56 | classes: vec![], 57 | attrs: vec![] 58 | })), 59 | "# " 60 | ); 61 | } 62 | #[test] 63 | fn header2() { 64 | assert_eq!( 65 | s(Start(Heading { 66 | level: HeadingLevel::H2, 67 | id: None, 68 | classes: vec![], 69 | attrs: vec![] 70 | })), 71 | "## " 72 | ); 73 | } 74 | #[test] 75 | fn blockquote() { 76 | assert_eq!(s(Start(BlockQuote(None))), "\n > "); 77 | assert_eq!(s(Start(BlockQuote(Some(BlockQuoteKind::Note)))), "\n > [!NOTE]"); 78 | assert_eq!(s(Start(BlockQuote(Some(BlockQuoteKind::Tip)))), "\n > [!TIP]"); 79 | assert_eq!( 80 | s(Start(BlockQuote(Some(BlockQuoteKind::Important)))), 81 | "\n > [!IMPORTANT]" 82 | ); 83 | assert_eq!(s(Start(BlockQuote(Some(BlockQuoteKind::Warning)))), "\n > [!WARNING]"); 84 | assert_eq!(s(Start(BlockQuote(Some(BlockQuoteKind::Caution)))), "\n > [!CAUTION]"); 85 | } 86 | #[test] 87 | fn codeblock() { 88 | assert_eq!( 89 | s(Start(CodeBlock(CodeBlockKind::Fenced("asdf".into())))), 90 | "\n````asdf\n" 91 | ); 92 | } 93 | #[test] 94 | fn list_unordered() { 95 | assert_eq!(s(Start(List(None))), ""); 96 | } 97 | #[test] 98 | fn list_ordered() { 99 | assert_eq!(s(Start(List(Some(1)))), ""); 100 | } 101 | #[test] 102 | fn item() { 103 | assert_eq!(s(Start(Item)), ""); 104 | } 105 | #[test] 106 | fn footnote_definition() { 107 | assert_eq!(s(Start(FootnoteDefinition("asdf".into()))), "[^asdf]: "); 108 | } 109 | #[test] 110 | fn emphasis() { 111 | assert_eq!(s(Start(Emphasis)), "*"); 112 | } 113 | #[test] 114 | fn strong() { 115 | assert_eq!(s(Start(Strong)), "**"); 116 | } 117 | #[test] 118 | fn link() { 119 | assert_eq!( 120 | s(Start(Link { 121 | link_type: Inline, 122 | dest_url: "uri".into(), 123 | title: "title".into(), 124 | id: "".into(), 125 | })), 126 | "[" 127 | ); 128 | } 129 | #[test] 130 | fn link_without_title() { 131 | assert_eq!( 132 | s(Start(Link { 133 | link_type: Inline, 134 | dest_url: "uri".into(), 135 | title: "".into(), 136 | id: "".into() 137 | })), 138 | "[" 139 | ); 140 | } 141 | #[test] 142 | fn image() { 143 | assert_eq!( 144 | s(Start(Image { 145 | link_type: Inline, 146 | dest_url: "uri".into(), 147 | title: "title".into(), 148 | id: "".into() 149 | })), 150 | "![" 151 | ); 152 | } 153 | #[test] 154 | fn image_without_title() { 155 | assert_eq!( 156 | s(Start(Image { 157 | link_type: Inline, 158 | dest_url: "uri".into(), 159 | title: "".into(), 160 | id: "".into() 161 | })), 162 | "![" 163 | ); 164 | } 165 | #[test] 166 | fn table() { 167 | assert_eq!(s(Start(Table(vec![Left, Center, Right, Alignment::None]))), ""); 168 | } 169 | #[test] 170 | fn table_head() { 171 | assert_eq!(s(Start(TableHead)), ""); 172 | } 173 | #[test] 174 | fn table_row() { 175 | assert_eq!(s(Start(TableRow)), ""); 176 | } 177 | #[test] 178 | fn table_cell() { 179 | assert_eq!(s(Start(TableCell)), "|"); 180 | } 181 | #[test] 182 | fn table_pipe() { 183 | assert_eq!( 184 | es([ 185 | Start(Table(vec![Left, Center, Right, Alignment::None])), 186 | Start(TableHead), 187 | Start(TableCell), 188 | Text("a|b".into()), 189 | ]), 190 | r"|a\|b" 191 | ); 192 | } 193 | 194 | #[test] 195 | fn definition_list_definition() { 196 | assert_eq!(s(Start(DefinitionListDefinition)), ": "); 197 | } 198 | } 199 | 200 | mod end { 201 | use pulldown_cmark::{BlockQuoteKind, CodeBlockKind, CowStr, Event::*, HeadingLevel, LinkType::*, Tag, TagEnd}; 202 | 203 | use super::{es, s}; 204 | 205 | #[test] 206 | fn header() { 207 | let tag = Tag::Heading { 208 | level: HeadingLevel::H2, 209 | id: None, 210 | classes: Default::default(), 211 | attrs: Default::default(), 212 | }; 213 | assert_eq!(es([Start(tag.clone()), End(tag.to_end())]), "## "); 214 | } 215 | #[test] 216 | fn paragraph() { 217 | assert_eq!(s(End(TagEnd::Paragraph)), ""); 218 | } 219 | #[test] 220 | fn blockquote() { 221 | assert_eq!(s(End(TagEnd::BlockQuote(None))), ""); 222 | assert_eq!( 223 | es([ 224 | Start(Tag::BlockQuote(Some(BlockQuoteKind::Note))), 225 | Text(CowStr::Borrowed("This is a note")), 226 | End(TagEnd::BlockQuote(Some(BlockQuoteKind::Note))) 227 | ]), 228 | "\n > [!NOTE]\n > This is a note" 229 | ); 230 | } 231 | #[test] 232 | fn codeblock() { 233 | assert_eq!( 234 | es([ 235 | Start(Tag::CodeBlock(CodeBlockKind::Fenced("".into()))), 236 | End(TagEnd::CodeBlock) 237 | ]), 238 | "\n````\n````" 239 | ); 240 | } 241 | #[test] 242 | fn codeblock_in_list_item() { 243 | assert_eq!( 244 | es([ 245 | Start(Tag::List(None)), 246 | Start(Tag::Item), 247 | Start(Tag::CodeBlock(CodeBlockKind::Fenced("".into()))), 248 | Text("foo".into()), 249 | End(TagEnd::CodeBlock), 250 | End(TagEnd::Item), 251 | End(TagEnd::List(false)), 252 | Start(Tag::Paragraph), 253 | Text("bar".into()), 254 | End(TagEnd::Paragraph), 255 | ]), 256 | "* \n ````\n foo\n ````\n\nbar" 257 | ); 258 | } 259 | #[test] 260 | fn codeblock_indented_in_list_item() { 261 | assert_eq!( 262 | es([ 263 | Start(Tag::List(None)), 264 | Start(Tag::Item), 265 | Start(Tag::CodeBlock(CodeBlockKind::Indented)), 266 | Text("foo".into()), 267 | End(TagEnd::CodeBlock), 268 | End(TagEnd::Item), 269 | End(TagEnd::List(false)), 270 | Start(Tag::Paragraph), 271 | Text("bar".into()), 272 | End(TagEnd::Paragraph), 273 | ]), 274 | "* \n foo\n \n\nbar" 275 | ); 276 | } 277 | #[test] 278 | fn footnote_definition() { 279 | assert_eq!(s(End(TagEnd::FootnoteDefinition)), ""); 280 | } 281 | #[test] 282 | fn emphasis() { 283 | assert_eq!(s(End(TagEnd::Emphasis)), "*"); 284 | } 285 | #[test] 286 | fn strong() { 287 | assert_eq!(s(End(TagEnd::Strong)), "**"); 288 | } 289 | #[test] 290 | fn list_unordered() { 291 | assert_eq!(s(End(TagEnd::List(false))), ""); 292 | } 293 | #[test] 294 | fn list_ordered() { 295 | assert_eq!(s(End(TagEnd::List(true))), ""); 296 | } 297 | #[test] 298 | fn item() { 299 | assert_eq!(s(End(TagEnd::Item)), ""); 300 | } 301 | #[test] 302 | fn link() { 303 | let tag = Tag::Link { 304 | link_type: Inline, 305 | dest_url: "/uri".into(), 306 | title: "title".into(), 307 | id: "".into(), 308 | }; 309 | assert_eq!(es([Start(tag.clone()), End(tag.to_end())]), "[](/uri \"title\")"); 310 | } 311 | #[test] 312 | fn link_without_title() { 313 | let tag = Tag::Link { 314 | link_type: Inline, 315 | dest_url: "/uri".into(), 316 | title: "".into(), 317 | id: "".into(), 318 | }; 319 | assert_eq!(es([Start(tag.clone()), End(tag.to_end())]), "[](/uri)"); 320 | } 321 | #[test] 322 | fn image() { 323 | let tag = Tag::Image { 324 | link_type: Inline, 325 | dest_url: "/uri".into(), 326 | title: "title".into(), 327 | id: "".into(), 328 | }; 329 | assert_eq!(es([Start(tag.clone()), End(tag.to_end())]), "![](/uri \"title\")"); 330 | } 331 | #[test] 332 | fn image_without_title() { 333 | let tag = Tag::Image { 334 | link_type: Inline, 335 | dest_url: "/uri".into(), 336 | title: "".into(), 337 | id: "".into(), 338 | }; 339 | assert_eq!(es([Start(tag.clone()), End(tag.to_end())]), "![](/uri)"); 340 | } 341 | #[test] 342 | fn table() { 343 | assert_eq!(s(End(TagEnd::Table)), ""); 344 | } 345 | #[test] 346 | fn table_row() { 347 | assert_eq!(s(End(TagEnd::TableRow)), "|"); 348 | } 349 | #[test] 350 | fn table_cell() { 351 | assert_eq!(s(End(TagEnd::TableCell)), ""); 352 | } 353 | } 354 | 355 | #[test] 356 | fn hardbreak() { 357 | assert_eq!(s(Event::HardBreak), " \n"); 358 | } 359 | #[test] 360 | fn softbreak() { 361 | assert_eq!(s(Event::SoftBreak), "\n"); 362 | } 363 | #[test] 364 | fn html() { 365 | assert_eq!(s(Event::Html("hi
".into())), "hi
"); 366 | } 367 | #[test] 368 | fn text() { 369 | assert_eq!(s(Event::Text("asdf".into())), "asdf"); 370 | } 371 | #[test] 372 | fn footnote_reference() { 373 | assert_eq!(s(Event::FootnoteReference("asdf".into())), "[^asdf]"); 374 | } 375 | #[test] 376 | fn math() { 377 | assert_eq!( 378 | s(Event::InlineMath(r"\sqrt{3x-1}+(1+x)^2".into())), 379 | r"$\sqrt{3x-1}+(1+x)^2$" 380 | ); 381 | assert_eq!(s(Event::InlineMath(r"\sqrt{\$4}".into())), r"$\sqrt{\$4}$"); 382 | assert_eq!(s( 383 | Event::DisplayMath( 384 | r"\left( \sum_{k=1}^n a_k b_k \right)^2 \leq \left( \sum_{k=1}^n a_k^2 \right) \left( \sum_{k=1}^n b_k^2 \right)".into() 385 | )), 386 | r"$$\left( \sum_{k=1}^n a_k b_k \right)^2 \leq \left( \sum_{k=1}^n a_k^2 \right) \left( \sum_{k=1}^n b_k^2 \right)$$" 387 | ); 388 | } 389 | -------------------------------------------------------------------------------- /tests/integrate/fmt.rs: -------------------------------------------------------------------------------- 1 | use pulldown_cmark::{utils::TextMergeStream, Alignment, CodeBlockKind, Event, LinkType, Options, Parser, Tag, TagEnd}; 2 | pub use pulldown_cmark_to_cmark::{ 3 | cmark, cmark_resume, cmark_resume_with_options, Options as CmarkToCmarkOptions, State, 4 | }; 5 | 6 | fn assert_output_and_states_eq(output0: &str, state0: &State, output1: &str, state1: &State) { 7 | assert_eq!( 8 | output0, output1, 9 | "Output of formatting without and with source range differs!" 10 | ); 11 | assert_eq!( 12 | state0, state1, 13 | "States of formatting without and with source range differs!" 14 | ); 15 | } 16 | 17 | fn fmts_both(s: &str) -> (String, State<'_>) { 18 | let (buf0, s0) = fmts(s); 19 | let (buf1, s1) = source_range::fmts(s); 20 | assert_output_and_states_eq(&buf0, &s0, &buf1, &s1); 21 | (buf0, s0) 22 | } 23 | 24 | fn fmts(s: &str) -> (String, State<'_>) { 25 | let mut buf = String::new(); 26 | let s = cmark(Parser::new_ext(s, Options::all()), &mut buf).unwrap(); 27 | (buf, s) 28 | } 29 | 30 | fn fmts_with_options<'a>(s: &'a str, options: CmarkToCmarkOptions<'a>) -> (String, State<'a>) { 31 | let (buf1, s1) = source_range::fmts_with_options(s, options.clone()); 32 | let mut buf = String::new(); 33 | let s = cmark_resume_with_options(Parser::new_ext(s, Options::all()), &mut buf, None, options).unwrap(); 34 | assert_output_and_states_eq(&buf, &s, &buf1, &s1); 35 | (buf, s) 36 | } 37 | 38 | fn fmtes<'a>(e: &'a [Event], s: State<'a>) -> (String, State<'a>) { 39 | let mut buf = String::new(); 40 | let s = cmark_resume(e.iter(), &mut buf, Some(s)).unwrap(); 41 | (buf, s) 42 | } 43 | 44 | fn fmte<'a>(e: impl AsRef<[Event<'a>]>) -> (String, State<'a>) { 45 | let mut buf = String::new(); 46 | let s = cmark(e.as_ref().iter(), &mut buf).unwrap(); 47 | (buf, s) 48 | } 49 | 50 | fn assert_events_eq_both(s: &str) { 51 | assert_events_eq(s); 52 | source_range::assert_events_eq(s); 53 | } 54 | 55 | /// Asserts that if we parse our `str` s into a series of events, then serialize them with `cmark` 56 | /// that we'll get the same series of events when we parse them again. 57 | fn assert_events_eq(s: &str) { 58 | let before_events = Parser::new_ext(s, Options::all()); 59 | 60 | let mut buf = String::new(); 61 | cmark(before_events, &mut buf).unwrap(); 62 | 63 | let before_events = TextMergeStream::new(Parser::new_ext(s, Options::all())); 64 | let after_events = TextMergeStream::new(Parser::new_ext(&buf, Options::all())); 65 | println!("{buf}"); 66 | assert_eq!(before_events.collect::>(), after_events.collect::>()); 67 | } 68 | 69 | mod lazy_newlines { 70 | use super::{fmte, fmts_both, Event, LinkType, State, Tag, TagEnd}; 71 | 72 | #[test] 73 | fn after_emphasis_there_is_no_newline() { 74 | for t in [ 75 | Tag::Emphasis, 76 | Tag::Strong, 77 | Tag::Link { 78 | link_type: LinkType::Inline, 79 | dest_url: "".into(), 80 | title: "".into(), 81 | id: "".into(), 82 | }, 83 | Tag::Image { 84 | link_type: LinkType::Inline, 85 | dest_url: "".into(), 86 | title: "".into(), 87 | id: "".into(), 88 | }, 89 | Tag::FootnoteDefinition("".into()), 90 | ] { 91 | let end = t.to_end(); 92 | let mut state = State::default(); 93 | state.newlines_before_start = 0; 94 | assert_eq!(fmte(&[Event::Start(t), Event::End(end)]).1, state); 95 | } 96 | } 97 | 98 | #[test] 99 | fn after_anything_else_it_has_one_newline() { 100 | for e in &[ 101 | Event::End(TagEnd::Item), 102 | Event::End(TagEnd::TableRow), 103 | Event::End(TagEnd::TableHead), 104 | ] { 105 | let mut state = State::default(); 106 | state.newlines_before_start = 1; 107 | assert_eq!(fmte(&[e.clone()]).1, state); 108 | } 109 | } 110 | 111 | #[test] 112 | fn after_some_types_it_has_multiple_newlines() { 113 | for md in &["paragraph", "## headline", "\n````\n````", "---"] { 114 | let mut state = State::default(); 115 | state.newlines_before_start = 2; 116 | assert_eq!(fmts_both(md), (String::from(*md), state)); 117 | } 118 | } 119 | } 120 | 121 | #[test] 122 | fn it_applies_newlines_before_start_before_text() { 123 | let mut first = State::default(); 124 | first.newlines_before_start = 2; 125 | first.last_was_text_without_trailing_newline = true; 126 | 127 | let mut second = State::default(); 128 | second.newlines_before_start; 129 | second.last_was_text_without_trailing_newline = true; 130 | 131 | assert_eq!(fmtes(&[Event::Text("t".into())], first), ("\n\nt".into(), second)); 132 | } 133 | 134 | #[test] 135 | fn it_applies_newlines_before_start_before_any_start_tag() { 136 | let mut first = State::default(); 137 | first.newlines_before_start = 2; 138 | first.last_was_text_without_trailing_newline = true; 139 | 140 | let mut second = State::default(); 141 | second.newlines_before_start = 0; 142 | second.last_was_text_without_trailing_newline = true; 143 | 144 | assert_eq!( 145 | fmtes(&[Event::Start(Tag::Paragraph), Event::Text("h".into())], first,), 146 | ("\n\nh".into(), second) 147 | ); 148 | } 149 | 150 | mod padding { 151 | use super::{fmtes, Event, State, Tag}; 152 | 153 | #[test] 154 | fn is_used_before_newlines() { 155 | let mut first = State::default(); 156 | first.newlines_before_start = 2; 157 | first.padding = vec![" ".into()]; 158 | first.last_was_text_without_trailing_newline = true; 159 | 160 | let mut second = State::default(); 161 | second.newlines_before_start = 0; 162 | second.padding = vec![" ".into()]; 163 | second.last_was_text_without_trailing_newline = true; 164 | 165 | assert_eq!( 166 | fmtes(&[Event::Start(Tag::Paragraph), Event::Text("h".into())], first,), 167 | ("\n \n h".into(), second) 168 | ); 169 | } 170 | } 171 | 172 | mod inline_elements { 173 | use crate::fmt::fmts_with_options; 174 | 175 | use super::source_range; 176 | use super::{fmts_both, CmarkToCmarkOptions, State}; 177 | 178 | #[test] 179 | fn image() { 180 | let mut state = State::default(); 181 | state.newlines_before_start = 2; 182 | assert_eq!( 183 | fmts_both("![a](b)\n![c][d]\n\n[d]: e"), 184 | ("![a](b)\n![c][d]\n\n[d]: e".into(), state) 185 | ); 186 | } 187 | 188 | #[test] 189 | fn image_collapsed() { 190 | let mut state = State::default(); 191 | state.newlines_before_start = 2; 192 | assert_eq!( 193 | fmts_both("![c][d]\n\n![c][]![c][]\n\n[d]: e\n[c]: f"), 194 | ("![c][d]\n\n![c][]![c][]\n\n[d]: e\n[c]: f".into(), state) 195 | ); 196 | } 197 | 198 | #[test] 199 | fn footnote() { 200 | let mut state = State::default(); 201 | state.newlines_before_start = 2; 202 | assert_eq!(fmts_both("a [^b]\n\n[^b]: c"), ("a [^b]\n\n[^b]: c".into(), state)); 203 | } 204 | 205 | #[test] 206 | fn multiline_footnote() { 207 | assert_eq!( 208 | fmts_both("a [^b]\n\n[^b]: this is\n one footnote").0, 209 | "a [^b]\n\n[^b]: this is\n one footnote", 210 | ); 211 | } 212 | 213 | #[test] 214 | fn autolinks_are_fully_resolved() { 215 | assert_eq!(fmts_both("").0, "",); 216 | } 217 | 218 | #[test] 219 | fn links() { 220 | { 221 | let mut state = State::default(); 222 | state.newlines_before_start = 2; 223 | assert_eq!( 224 | fmts_both("[a](b)\n[c][d]\n\n[d]: e"), 225 | ("[a](b)\n[c][d]\n\n[d]: e".into(), state) 226 | ); 227 | } 228 | } 229 | 230 | #[test] 231 | fn links_collapsed() { 232 | let mut state = State::default(); 233 | state.newlines_before_start = 2; 234 | assert_eq!( 235 | fmts_both("[c][d]\n\n[c][][c][]\n\n[d]: e\n[c]: f"), 236 | ("[c][d]\n\n[c][][c][]\n\n[d]: e\n[c]: f".into(), state) 237 | ); 238 | } 239 | 240 | #[test] 241 | fn shortcut_links() { 242 | { 243 | let mut state = State::default(); 244 | state.newlines_before_start = 2; 245 | assert_eq!( 246 | fmts_both("[a](b)\n[c]\n\n[c]: e"), 247 | ("[a](b)\n[c]\n\n[c]: e".into(), state) 248 | ); 249 | } 250 | } 251 | 252 | #[test] 253 | fn shortcut_code_links() { 254 | let mut state = State::default(); 255 | state.newlines_before_start = 2; 256 | assert_eq!( 257 | fmts_both("[a](b)\n[`c`]\n\n[`c`]: e"), 258 | ("[a](b)\n[`c`]\n\n[`c`]: e".into(), state) 259 | ); 260 | } 261 | 262 | #[test] 263 | fn multiple_shortcut_links() { 264 | let mut state = State::default(); 265 | state.newlines_before_start = 2; 266 | assert_eq!( 267 | fmts_both("[a](b)\n[c] [d]\n\n[c]: e\n[d]: f"), 268 | ("[a](b)\n[c] [d]\n\n[c]: e\n[d]: f".into(), state) 269 | ); 270 | } 271 | 272 | #[test] 273 | fn various() { 274 | let mut state = State::default(); 275 | state.newlines_before_start = 2; 276 | assert_eq!( 277 | fmts_both("*a* b **c**\n
\nd\n\ne `c`"), 278 | ("*a* b **c**\n
\nd\n\ne `c`".into(), state) 279 | ); 280 | } 281 | 282 | #[test] 283 | fn various_with_custom_options() { 284 | let custom_options = CmarkToCmarkOptions { 285 | emphasis_token: '_', 286 | code_block_token: '~', 287 | ..Default::default() 288 | }; 289 | 290 | let (s, state) = fmts_with_options("_a_ b **c**\n
\nd\n\ne `c`", custom_options); 291 | 292 | assert_eq!(s, "_a_ b **c**\n
\nd\n\ne `c`".to_string()); 293 | 294 | let mut expected = State::default(); 295 | expected.newlines_before_start = 2; 296 | assert_eq!(state, expected); 297 | } 298 | 299 | #[test] 300 | fn strikethrough() { 301 | assert_eq!(fmts_both("~~strikethrough~~").0, "~~strikethrough~~",); 302 | } 303 | 304 | #[test] 305 | fn code_double_backtick() { 306 | let mut state = State::default(); 307 | state.newlines_before_start = 2; 308 | assert_eq!( 309 | fmts_both("lorem ``ipsum `dolor` sit`` amet"), 310 | ("lorem ``ipsum `dolor` sit`` amet".into(), state) 311 | ); 312 | } 313 | 314 | #[test] 315 | fn code_triple_backtick() { 316 | let mut state = State::default(); 317 | state.newlines_before_start = 2; 318 | assert_eq!( 319 | fmts_both("lorem ```ipsum ``dolor`` sit``` amet"), 320 | ("lorem ```ipsum ``dolor`` sit``` amet".into(), state) 321 | ); 322 | } 323 | 324 | #[test] 325 | fn code_backtick_normalization() { 326 | // The minimum amount of backticks are inserted. 327 | let mut state = State::default(); 328 | state.newlines_before_start = 2; 329 | assert_eq!( 330 | fmts_both("lorem ```ipsum ` dolor``` amet"), 331 | ("lorem ``ipsum ` dolor`` amet".into(), state) 332 | ); 333 | } 334 | 335 | #[test] 336 | fn code_leading_trailing_backtick() { 337 | // Spaces are inserted if the inline code starts or ends with 338 | // a backtick. 339 | { 340 | let mut state = State::default(); 341 | state.newlines_before_start = 2; 342 | assert_eq!( 343 | fmts_both("`` `lorem `` `` ipsum` ``"), 344 | ("`` `lorem `` `` ipsum` ``".into(), state) 345 | ); 346 | } 347 | } 348 | 349 | #[test] 350 | fn code_spaces_before_backtick() { 351 | // No space is inserted if it is not needed. 352 | { 353 | let mut state = State::default(); 354 | state.newlines_before_start = 2; 355 | assert_eq!(fmts_both("` lorem ` ` `"), ("`lorem` ` `".into(), state)); 356 | } 357 | } 358 | 359 | #[test] 360 | fn no_escaping_special_character_in_code() { 361 | // https://github.com/Byron/pulldown-cmark-to-cmark/issues/73 362 | let input = r#" 363 | ```rust 364 | # fn main() { 365 | println!("Hello, world!"); 366 | # } 367 | ``` 368 | "#; 369 | let iter = pulldown_cmark::Parser::new(input); 370 | let mut actual = String::new(); 371 | pulldown_cmark_to_cmark::cmark_with_source_range_and_options( 372 | iter.map(|e| (e, None)), 373 | input, 374 | &mut actual, 375 | Default::default(), 376 | ) 377 | .unwrap(); 378 | let expected = r#" 379 | ````rust 380 | # fn main() { 381 | println!("Hello, world!"); 382 | # } 383 | ````"#; 384 | assert_eq!(actual, expected); 385 | } 386 | 387 | #[test] 388 | fn rustdoc_link() { 389 | // Brackets are not escaped if not escaped in the source. 390 | { 391 | let mut state = State::default(); 392 | state.newlines_before_start = 2; 393 | assert_eq!(source_range::fmts("[`Vec`]"), ("[`Vec`]".into(), state)); 394 | } 395 | } 396 | 397 | #[test] 398 | fn preserve_less_than_sign_escape() { 399 | // `<` is not escaped if not escaped in the source. 400 | let mut state = State::default(); 401 | state.newlines_before_start = 2; 402 | assert_eq!(source_range::fmts("a < 1"), ("a < 1".into(), state)); 403 | // `<` is escaped if escaped in the source. 404 | let mut state = State::default(); 405 | state.newlines_before_start = 2; 406 | assert_eq!(source_range::fmts(r"a \< 1"), (r"a \< 1".into(), state)); 407 | } 408 | } 409 | 410 | mod blockquote { 411 | use super::{assert_events_eq_both, fmte, fmtes, fmts_both, Event, State, Tag, TagEnd}; 412 | use indoc::indoc; 413 | 414 | #[test] 415 | fn it_pops_padding_on_quote_end() { 416 | let mut first = State::default(); 417 | first.padding = vec![" > ".into()]; 418 | 419 | let mut second = State::default(); 420 | second.newlines_before_start = 2; 421 | second.padding = vec![]; 422 | 423 | assert_eq!(fmtes(&[Event::End(TagEnd::BlockQuote(None)),], first,).1, second); 424 | } 425 | 426 | #[test] 427 | fn it_pushes_padding_on_quote_start() { 428 | let mut state = State::default(); 429 | state.newlines_before_start = 1; 430 | state.padding = vec![" > ".into()]; 431 | assert_eq!(fmte(&[Event::Start(Tag::BlockQuote(None)),]).1, state); 432 | } 433 | 434 | #[test] 435 | fn with_html() { 436 | let s = indoc!( 437 | " 438 | > 439 | >
440 | " 441 | ); 442 | 443 | assert_events_eq_both(s); 444 | 445 | assert_eq!(fmts_both(s).0, "\n > \n > \n >
\n > "); 446 | } 447 | 448 | #[test] 449 | fn with_inlinehtml() { 450 | assert_eq!(fmts_both(" >
").0, "\n > \n >
"); 451 | } 452 | 453 | #[test] 454 | fn with_plaintext_in_html() { 455 | assert_eq!(fmts_both("\n*foo*\n").0, "\n*foo*\n"); 456 | } 457 | 458 | #[test] 459 | fn with_markdown_nested_in_html() { 460 | assert_eq!(fmts_both("\n\n*foo*\n\n").0, "\n\n*foo*\n\n"); 461 | } 462 | 463 | #[test] 464 | fn with_codeblock() { 465 | let s = indoc!( 466 | " 467 | > ```a 468 | > t1 469 | > t2 470 | > ``` 471 | " 472 | ); 473 | 474 | assert_events_eq_both(s); 475 | 476 | assert_eq!(fmts_both(s).0, "\n > \n > ````a\n > t1\n > t2\n > ````",); 477 | } 478 | 479 | #[test] 480 | fn nested() { 481 | let s = indoc!( 482 | " 483 | > a 484 | > 485 | > > b 486 | > 487 | > c 488 | " 489 | ); 490 | 491 | assert_events_eq_both(s); 492 | 493 | assert_eq!(fmts_both(s).0, "\n > \n > a\n > \n > > \n > > b\n > \n > c",); 494 | } 495 | 496 | #[test] 497 | fn initially_nested() { 498 | let s = indoc!( 499 | " 500 | > > foo 501 | > bar 502 | > > baz 503 | " 504 | ); 505 | 506 | assert_events_eq_both(s); 507 | 508 | assert_eq!(fmts_both(s).0, "\n > \n > > \n > > foo\n > > bar\n > > baz",); 509 | } 510 | 511 | #[test] 512 | fn simple() { 513 | // Inlining this rather than using indoc because format-on-save with 514 | // rustfmt tries to strip the trailing spaces after `b` otherwise in 515 | // some editors. 516 | let s = "> a\n> b \n> c\n"; 517 | 518 | assert_events_eq_both(s); 519 | 520 | { 521 | let mut state = State::default(); 522 | state.newlines_before_start = 2; 523 | assert_eq!(fmts_both(s), ("\n > \n > a\n > b \n > c".into(), state)); 524 | } 525 | } 526 | 527 | #[test] 528 | fn empty() { 529 | let s = " > "; 530 | 531 | assert_events_eq_both(s); 532 | 533 | { 534 | let mut state = State::default(); 535 | state.newlines_before_start = 2; 536 | assert_eq!(fmts_both(s), ("\n > ".into(), state)); 537 | } 538 | } 539 | 540 | #[test] 541 | fn with_blank_line() { 542 | let s = indoc!( 543 | " 544 | > foo 545 | 546 | > bar 547 | " 548 | ); 549 | 550 | assert_events_eq_both(s); 551 | 552 | let mut state = State::default(); 553 | state.newlines_before_start = 2; 554 | assert_eq!(fmts_both(s), ("\n > \n > foo\n\n > \n > bar".into(), state)); 555 | } 556 | 557 | #[test] 558 | fn with_lazy_continuation() { 559 | let s = indoc!( 560 | " 561 | > foo 562 | baz 563 | 564 | > bar 565 | " 566 | ); 567 | 568 | assert_events_eq_both(s); 569 | 570 | let mut state = State::default(); 571 | state.newlines_before_start = 2; 572 | assert_eq!(fmts_both(s), ("\n > \n > foo\n > baz\n\n > \n > bar".into(), state)); 573 | } 574 | 575 | #[test] 576 | fn with_lists() { 577 | let s = indoc!( 578 | " 579 | - > * foo 580 | > * baz 581 | - > bar 582 | " 583 | ); 584 | 585 | assert_events_eq_both(s); 586 | 587 | let mut state = State::default(); 588 | state.newlines_before_start = 2; 589 | assert_eq!( 590 | fmts_both(s), 591 | ( 592 | "* \n > \n > * foo\n > * baz\n \n * \n > \n > bar".into(), 593 | state 594 | ) 595 | ); 596 | } 597 | 598 | #[test] 599 | fn complex_nesting() { 600 | assert_events_eq_both(indoc!( 601 | " 602 | > one 603 | > > two 604 | > > three 605 | > four 606 | > 607 | > > five 608 | > 609 | > > six 610 | > seven 611 | > > > eight 612 | nine 613 | 614 | > ten 615 | 616 | > 617 | 618 | > 619 | > > 620 | 621 | 622 | > > 623 | 624 | > - eleven 625 | > - twelve 626 | > > thirteen 627 | > - 628 | 629 | - > fourteen 630 | - > fifteen 631 | " 632 | )); 633 | } 634 | } 635 | 636 | mod codeblock { 637 | use super::{fmte, fmts_both, fmts_with_options, CmarkToCmarkOptions, CodeBlockKind, Event, State, Tag}; 638 | 639 | #[test] 640 | fn it_keeps_track_of_the_presence_of_a_code_block() { 641 | let mut state = State::default(); 642 | state.code_block = Some(pulldown_cmark_to_cmark::CodeBlockKind::Fenced); 643 | assert_eq!( 644 | fmte(&[Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced("s".into()))),]).1, 645 | state 646 | ); 647 | } 648 | 649 | #[test] 650 | fn simple_and_paragraph() { 651 | let mut state = State::default(); 652 | state.newlines_before_start = 2; 653 | assert_eq!( 654 | fmts_both("````hi\nsome\ntext\n````\na"), 655 | ("\n````hi\nsome\ntext\n````\n\na".into(), state) 656 | ); 657 | } 658 | 659 | #[test] 660 | fn empty() { 661 | { 662 | let mut state = State::default(); 663 | state.newlines_before_start = 2; 664 | assert_eq!(fmts_both("```\n```"), ("\n````\n````".into(), state)); 665 | } 666 | } 667 | 668 | #[test] 669 | fn simple() { 670 | let mut state = State::default(); 671 | state.newlines_before_start = 2; 672 | assert_eq!( 673 | fmts_both("```hi\nsome\ntext\n```"), 674 | ("\n````hi\nsome\ntext\n````".into(), state) 675 | ); 676 | } 677 | 678 | #[test] 679 | fn simple_other_syntax() { 680 | let mut state = State::default(); 681 | state.newlines_before_start = 2; 682 | assert_eq!( 683 | fmts_both("~~~hi\nsome\ntext\n~~~"), 684 | ("\n````hi\nsome\ntext\n````".into(), state) 685 | ); 686 | } 687 | 688 | #[test] 689 | fn simple_other_syntax_with_custom() { 690 | let custom_options = CmarkToCmarkOptions { 691 | code_block_token: '~', 692 | ..Default::default() 693 | }; 694 | 695 | let original = "~~~hi\nsome\ntext\n~~~"; 696 | let (s, _) = fmts_with_options(original, custom_options); 697 | 698 | assert_eq!(s, "\n~~~~hi\nsome\ntext\n~~~~".to_string()); 699 | } 700 | 701 | #[test] 702 | fn indented() { 703 | let mut state = State::default(); 704 | state.newlines_before_start = 2; 705 | assert_eq!( 706 | fmts_both(" first\n second\nthird"), 707 | ("\n first\n second\n \n\nthird".into(), state) 708 | ); 709 | } 710 | 711 | #[test] 712 | fn html_indented() { 713 | let mut state = State::default(); 714 | state.newlines_before_start = 2; 715 | assert_eq!( 716 | fmts_both(" \n\n "), 717 | (" \n\n \n ".into(), state) 718 | ); 719 | } 720 | } 721 | 722 | mod table { 723 | use indoc::indoc; 724 | use pretty_assertions::assert_eq; 725 | use pulldown_cmark_to_cmark::Alignment; 726 | 727 | use super::{fmte, fmtes, Alignment as TableAlignment, Event, State, Tag, TagEnd}; 728 | 729 | #[test] 730 | fn it_forgets_alignments_and_headers_at_the_end_of_tables() { 731 | let mut first = State::default(); 732 | first.table_alignments = vec![Alignment::None, Alignment::Center]; 733 | first.table_headers = vec!["a".into(), "b".into()]; 734 | 735 | let mut second = State::default(); 736 | second.newlines_before_start = 2; 737 | 738 | assert_eq!(fmtes(&[Event::End(TagEnd::Table),], first,).1, second); 739 | } 740 | 741 | #[test] 742 | fn it_keeps_track_of_alignments_and_headers() { 743 | let mut state = State::default(); 744 | state.table_alignments = vec![Alignment::None, Alignment::Center]; 745 | state.table_headers = vec!["a".into(), "b".into()]; 746 | assert_eq!( 747 | fmte(&[ 748 | Event::Start(Tag::Table(vec![TableAlignment::None, TableAlignment::Center])), 749 | Event::Start(Tag::TableHead), 750 | Event::Start(Tag::TableCell), 751 | Event::Text("a".into()), 752 | Event::End(TagEnd::TableCell), 753 | Event::Start(Tag::TableCell), 754 | Event::Text("b".into()), 755 | Event::End(TagEnd::TableCell), 756 | ]) 757 | .1, 758 | state 759 | ); 760 | } 761 | 762 | #[test] 763 | fn it_generates_equivalent_table_markdown() { 764 | use pulldown_cmark::{Options, Parser}; 765 | 766 | let original_table_markdown = indoc!( 767 | " 768 | | Tables | Are | Cool | yo || 769 | |---------------|:-------------:|------:|:---|--| 770 | | col 3 is | right-aligned | $1600 | x |01| 771 | | col 2 is | centered | $12 | y |02| 772 | | zebra stripes | are neat | $1 | z |03|" 773 | ); 774 | let p = Parser::new_ext(original_table_markdown, Options::all()); 775 | let original_events: Vec<_> = p.into_iter().collect(); 776 | 777 | let (generated_markdown, _) = fmte(&original_events); 778 | 779 | assert_eq!( 780 | generated_markdown, 781 | indoc!( 782 | " 783 | |Tables|Are|Cool|yo|| 784 | |------|:-:|---:|:-|-| 785 | |col 3 is|right-aligned|$1600|x|01| 786 | |col 2 is|centered|$12|y|02| 787 | |zebra stripes|are neat|$1|z|03|" 788 | ) 789 | ); 790 | 791 | let p = Parser::new_ext(&generated_markdown, Options::all()); 792 | let generated_events: Vec<_> = p.into_iter().collect(); 793 | 794 | assert_eq!(original_events, generated_events); 795 | } 796 | 797 | #[test] 798 | fn it_generates_equivalent_table_markdown_with_empty_headers() { 799 | use pulldown_cmark::{Options, Parser}; 800 | 801 | let original_table_markdown = indoc!( 802 | " 803 | |||||| 804 | |:-------------:|:--------------|------:|:--:|:-:| 805 | | col 3 is | right-aligned | $1600 | x |01| 806 | | col 2 is | centered | $12 | y |02| 807 | | zebra stripes | are neat | $1 | z |03|" 808 | ); 809 | let p = Parser::new_ext(original_table_markdown, Options::all()); 810 | let original_events: Vec<_> = p.into_iter().collect(); 811 | 812 | let (generated_markdown, _) = fmte(&original_events); 813 | 814 | assert_eq!( 815 | generated_markdown, 816 | indoc!( 817 | " 818 | |||||| 819 | |:-:|:-|-:|:-:|:-:| 820 | |col 3 is|right-aligned|$1600|x|01| 821 | |col 2 is|centered|$12|y|02| 822 | |zebra stripes|are neat|$1|z|03|" 823 | ) 824 | ); 825 | 826 | let p = Parser::new_ext(&generated_markdown, Options::all()); 827 | let generated_events: Vec<_> = p.into_iter().collect(); 828 | 829 | assert_eq!(original_events, generated_events); 830 | } 831 | #[test] 832 | fn table_with_pipe_in_column() { 833 | use pulldown_cmark::{Options, Parser}; 834 | 835 | let original_table_markdown = indoc!( 836 | r" 837 | | \| | a\|b | 838 | |----|------| 839 | | \| | a\|b |" 840 | ); 841 | let p = Parser::new_ext(original_table_markdown, Options::all()); 842 | let original_events: Vec<_> = p.into_iter().collect(); 843 | 844 | let (generated_markdown, _) = fmte(&original_events); 845 | 846 | assert_eq!( 847 | generated_markdown, 848 | indoc!( 849 | r" 850 | |\||a\|b| 851 | |-|---| 852 | |\||a\|b|" 853 | ) 854 | ); 855 | 856 | let p = Parser::new_ext(&generated_markdown, Options::all()); 857 | let generated_events: Vec<_> = p.into_iter().collect(); 858 | 859 | assert_eq!(original_events, generated_events); 860 | } 861 | } 862 | 863 | mod escapes { 864 | use pulldown_cmark::CowStr; 865 | 866 | use super::source_range; 867 | use crate::{fmt::fmts, fmt::fmts_both, fmt::CmarkToCmarkOptions, fmt::Event, fmt::Parser, fmt::Tag, fmt::TagEnd}; 868 | 869 | fn run_test_on_each_special_char(f: impl Fn(String, CowStr)) { 870 | for c in CmarkToCmarkOptions::default().special_characters().chars() { 871 | let s = format!(r#"\{c}"#); 872 | f(s, c.to_string().into()); 873 | } 874 | } 875 | 876 | #[test] 877 | fn it_does_not_recreate_escapes_for_underscores_in_the_middle_of_a_word() { 878 | assert_eq!( 879 | fmts("\\_hello_world_").0, 880 | "\\_hello_world\\_" // it actually makes mal-formatted markdown better 881 | ); 882 | } 883 | 884 | #[test] 885 | fn it_preserves_underscores_escapes() { 886 | assert_eq!(source_range::fmts("\\_hello_world_").0, "\\_hello_world_"); 887 | } 888 | 889 | #[test] 890 | fn it_recreates_escapes_for_known_special_characters_at_the_beginning_of_the_word() { 891 | run_test_on_each_special_char(|escaped_special_character, _| { 892 | assert_eq!(fmts_both(&escaped_special_character).0, escaped_special_character); 893 | }); 894 | } 895 | 896 | #[test] 897 | fn are_not_needed_for_underscores_within_a_word_and_no_spaces() { 898 | let e: Vec<_> = Parser::new("hello_there_and__hello again_").collect(); 899 | assert_eq!( 900 | e, 901 | vec![ 902 | Event::Start(Tag::Paragraph), 903 | Event::Text("hello_there_and__hello again".into()), 904 | Event::Text("_".into()), 905 | Event::End(TagEnd::Paragraph), 906 | ] 907 | ); 908 | } 909 | 910 | #[test] 911 | fn would_be_needed_for_single_backticks() { 912 | let e: Vec<_> = Parser::new(r"\`hi`").collect(); 913 | assert_eq!( 914 | e, 915 | vec![ 916 | Event::Start(Tag::Paragraph), 917 | Event::Text("`".into()), 918 | Event::Text("hi".into()), 919 | Event::Text("`".into()), 920 | Event::End(TagEnd::Paragraph), 921 | ] 922 | ); 923 | } 924 | 925 | #[test] 926 | fn it_escapes_closing_square_brackets() { 927 | assert_eq!( 928 | fmts_both(r"[\[1\]](http://example.com)").0, 929 | r"[\[1\]](http://example.com)" 930 | ); 931 | } 932 | 933 | #[test] 934 | fn link_titles() { 935 | // See https://spec.commonmark.org/0.30/#link-title for the rules around 936 | // link titles and the characters they may contain 937 | assert_eq!( 938 | fmts_both(r#"[link](http://example.com "'link title'")"#).0, 939 | r#"[link](http://example.com "'link title'")"# 940 | ); 941 | assert_eq!( 942 | fmts_both(r#"[link](http://example.com "\\\"link \\ title\"")"#).0, 943 | r#"[link](http://example.com "\\\"link \\ title\"")"# 944 | ); 945 | assert_eq!( 946 | fmts_both(r#"[link](http://example.com "\"link title\"")"#).0, 947 | r#"[link](http://example.com "\"link title\"")"# 948 | ); 949 | assert_eq!( 950 | fmts_both(r#"[link](http://example.com '"link title"')"#).0, 951 | r#"[link](http://example.com "\"link title\"")"# 952 | ); 953 | assert_eq!( 954 | fmts_both(r"[link](http://example.com '\'link title\'')").0, 955 | r#"[link](http://example.com "'link title'")"# 956 | ); 957 | assert_eq!( 958 | fmts_both(r"[link](http://example.com (\(link title\)))").0, 959 | r#"[link](http://example.com "(link title)")"# 960 | ); 961 | assert_eq!( 962 | fmts_both(r"[link](http://example.com (你好👋))").0, 963 | r#"[link](http://example.com "你好👋")"# 964 | ); 965 | } 966 | 967 | #[test] 968 | fn it_does_esscape_lone_square_brackets_in_text() { 969 | assert_eq!( 970 | fmts("] a closing bracket does nothing").0, 971 | "\\] a closing bracket does nothing" 972 | ); 973 | } 974 | 975 | #[test] 976 | fn it_does_not_escape_lone_square_brackets_in_text_if_the_source_does_not() { 977 | assert_eq!( 978 | source_range::fmts("] a closing bracket does nothing").0, 979 | "] a closing bracket does nothing" 980 | ); 981 | } 982 | 983 | #[test] 984 | fn make_special_characters_into_text_blocks() { 985 | let e: Vec<_> = Parser::new(r"hello\*there*and\*\*hello again\*\*").collect(); 986 | assert_eq!( 987 | e, 988 | vec![ 989 | Event::Start(Tag::Paragraph), 990 | Event::Text("hello".into()), 991 | Event::Text("*there".into()), 992 | Event::Text("*".into()), 993 | Event::Text("and".into()), 994 | Event::Text("*".into()), 995 | Event::Text("*hello again".into()), 996 | Event::Text("*".into()), 997 | Event::Text("*".into()), 998 | Event::End(TagEnd::Paragraph), 999 | ] 1000 | ); 1001 | } 1002 | 1003 | #[test] 1004 | fn would_be_needed_for_asterisks_within_a_word_and_no_spaces() { 1005 | let e: Vec<_> = Parser::new("hello*there*and**hello again**").collect(); 1006 | assert_eq!( 1007 | e, 1008 | vec![ 1009 | Event::Start(Tag::Paragraph), 1010 | Event::Text("hello".into()), 1011 | Event::Start(Tag::Emphasis), 1012 | Event::Text("there".into()), 1013 | Event::End(TagEnd::Emphasis), 1014 | Event::Text("and".into()), 1015 | Event::Start(Tag::Strong), 1016 | Event::Text("hello again".into()), 1017 | Event::End(TagEnd::Strong), 1018 | Event::End(TagEnd::Paragraph), 1019 | ] 1020 | ); 1021 | } 1022 | 1023 | #[test] 1024 | fn are_not_specifically_provided_as_events() { 1025 | run_test_on_each_special_char(|s, c| { 1026 | let e: Vec<_> = Parser::new(&s).collect(); 1027 | assert_eq!( 1028 | e, 1029 | vec![ 1030 | Event::Start(Tag::Paragraph), 1031 | Event::Text(c.to_string().into()), 1032 | Event::End(TagEnd::Paragraph), 1033 | ] 1034 | ); 1035 | }); 1036 | } 1037 | 1038 | #[test] 1039 | fn entity_escape_is_not_code_block_indent() { 1040 | source_range::assert_events_eq(" foo"); 1041 | source_range::assert_events_eq(" foo"); 1042 | source_range::assert_events_eq(" * foo\n * foo"); 1043 | } 1044 | } 1045 | 1046 | mod list { 1047 | use super::{fmtes, fmts_both, fmts_with_options, CmarkToCmarkOptions, Event, State, TagEnd}; 1048 | use indoc::indoc; 1049 | 1050 | #[test] 1051 | fn it_pops_one_item_from_the_lists_stack_for_each_end_list() { 1052 | let mut first = State::default(); 1053 | first.list_stack = vec![None, None]; 1054 | 1055 | let mut second = State::default(); 1056 | second.list_stack = vec![None]; 1057 | 1058 | assert_eq!(fmtes(&[Event::End(TagEnd::List(false))], first,).1, second); 1059 | } 1060 | 1061 | #[test] 1062 | fn ordered_and_unordered_nested_and_ordered() { 1063 | let mut state = State::default(); 1064 | state.newlines_before_start = 2; 1065 | assert_eq!( 1066 | fmts_both("1. *b*\n * *b*\n1. c"), 1067 | ("1. *b*\n * *b*\n1. c".into(), state) 1068 | ); 1069 | } 1070 | 1071 | #[test] 1072 | fn ordered_and_multiple_unordered() { 1073 | let mut state = State::default(); 1074 | state.newlines_before_start = 2; 1075 | assert_eq!( 1076 | fmts_both("11. *b*\n * *b*\n * c"), 1077 | ("11. *b*\n * *b*\n * c".into(), state) 1078 | ); 1079 | } 1080 | 1081 | #[test] 1082 | fn unordered_ordered_unordered() { 1083 | assert_eq!(fmts_both("* a\n 1. b\n* c").0, "* a\n 1. b\n* c",); 1084 | } 1085 | 1086 | #[test] 1087 | fn ordered_and_unordered_nested() { 1088 | let mut state = State::default(); 1089 | state.newlines_before_start = 2; 1090 | assert_eq!(fmts_both("1. *b*\n * *b*"), ("1. *b*\n * *b*".into(), state)); 1091 | } 1092 | 1093 | #[test] 1094 | fn unordered() { 1095 | let mut state = State::default(); 1096 | state.newlines_before_start = 2; 1097 | assert_eq!(fmts_both("* a\n* b"), ("* a\n* b".into(), state)); 1098 | } 1099 | 1100 | #[test] 1101 | fn unordered_with_custom() { 1102 | let custom_options = CmarkToCmarkOptions { 1103 | list_token: '-', 1104 | ..Default::default() 1105 | }; 1106 | 1107 | let original = "* a\n* b"; 1108 | let (s, _) = fmts_with_options(original, custom_options); 1109 | 1110 | assert_eq!(s, "- a\n- b".to_string()); 1111 | } 1112 | 1113 | #[test] 1114 | fn ordered() { 1115 | let mut state = State::default(); 1116 | state.newlines_before_start = 2; 1117 | assert_eq!(fmts_both("2. a\n2. b"), ("2. a\n2. b".into(), state)); 1118 | } 1119 | 1120 | #[test] 1121 | fn change_ordered_list_token() { 1122 | let custom_options = CmarkToCmarkOptions { 1123 | ordered_list_token: ')', 1124 | ..Default::default() 1125 | }; 1126 | let mut state = State::default(); 1127 | state.newlines_before_start = 2; 1128 | assert_eq!( 1129 | fmts_with_options("2. a\n2. b", custom_options), 1130 | ("2) a\n2) b".into(), state) 1131 | ); 1132 | } 1133 | 1134 | #[test] 1135 | fn increment_ordered_list_bullets() { 1136 | let custom_options = CmarkToCmarkOptions { 1137 | increment_ordered_list_bullets: true, 1138 | ..Default::default() 1139 | }; 1140 | let mut state = State::default(); 1141 | state.newlines_before_start = 2; 1142 | assert_eq!( 1143 | fmts_with_options("2. a\n2. b\n2. c", custom_options), 1144 | ("2. a\n3. b\n4. c".into(), state) 1145 | ); 1146 | } 1147 | 1148 | #[test] 1149 | fn nested_increment_ordered_list_bullets() { 1150 | let custom_options = CmarkToCmarkOptions { 1151 | increment_ordered_list_bullets: true, 1152 | ..Default::default() 1153 | }; 1154 | let input = indoc!( 1155 | " 1156 | 1. level 1 1157 | 1. level 2 1158 | 1. level 3 1159 | 1. level 3 1160 | 1. level 2 1161 | 1. level 1" 1162 | ); 1163 | 1164 | let expected = indoc!( 1165 | " 1166 | 1. level 1 1167 | 1. level 2 1168 | 1. level 3 1169 | 2. level 3 1170 | 2. level 2 1171 | 2. level 1" 1172 | ); 1173 | let mut state = State::default(); 1174 | state.newlines_before_start = 2; 1175 | assert_eq!(fmts_with_options(input, custom_options), (expected.into(), state)); 1176 | } 1177 | 1178 | #[test] 1179 | fn nested_increment_ordered_list_bullets_change_ordered_list_token() { 1180 | let custom_options = CmarkToCmarkOptions { 1181 | increment_ordered_list_bullets: true, 1182 | ordered_list_token: ')', 1183 | ..Default::default() 1184 | }; 1185 | let input = indoc!( 1186 | " 1187 | 1. level 1 1188 | 1. level 2 1189 | 1. level 3 1190 | 1. level 3 1191 | 1. level 2 1192 | 1. level 1 1193 | 1. level 1 1194 | 1. level 2 1195 | 1. level 3 1196 | 1. level 3 1197 | 1. level 2 1198 | 1. level 1" 1199 | ); 1200 | 1201 | let expected = indoc!( 1202 | " 1203 | 1) level 1 1204 | 1) level 2 1205 | 1) level 3 1206 | 2) level 3 1207 | 2) level 2 1208 | 2) level 1 1209 | 3) level 1 1210 | 1) level 2 1211 | 1) level 3 1212 | 2) level 3 1213 | 2) level 2 1214 | 4) level 1" 1215 | ); 1216 | let mut state = State::default(); 1217 | state.newlines_before_start = 2; 1218 | assert_eq!(fmts_with_options(input, custom_options), (expected.into(), state)); 1219 | } 1220 | 1221 | #[test] 1222 | fn checkboxes() { 1223 | assert_eq!( 1224 | fmts_both(indoc!( 1225 | " 1226 | * [ ] foo 1227 | * [x] bar 1228 | " 1229 | )) 1230 | .0, 1231 | "* [ ] foo\n* [x] bar", 1232 | ); 1233 | } 1234 | } 1235 | 1236 | mod heading { 1237 | use super::assert_events_eq_both; 1238 | 1239 | #[test] 1240 | fn heading_with_classes_and_attrs() { 1241 | assert_events_eq_both("# Heading { #id .class1 key1=val1 .class2 }"); 1242 | assert_events_eq_both("# Heading { #id .class1 .class2 key1=val1 key2 }"); 1243 | } 1244 | #[test] 1245 | fn heading_with_hashes_at_end() { 1246 | assert_events_eq_both("Heading #\n===="); 1247 | assert_events_eq_both("Heading \\#\n===="); 1248 | assert_events_eq_both("# Heading \\#"); 1249 | } 1250 | } 1251 | 1252 | mod frontmatter { 1253 | use pulldown_cmark::{Options, Parser}; 1254 | use pulldown_cmark_to_cmark::{cmark, cmark_with_options}; 1255 | 1256 | #[test] 1257 | fn yaml_frontmatter_should_be_supported() { 1258 | let input = "--- 1259 | key1: value1 1260 | key2: value2 1261 | --- 1262 | 1263 | # Frontmatter should be supported"; 1264 | 1265 | let mut opts = Options::empty(); 1266 | opts.insert(Options::ENABLE_YAML_STYLE_METADATA_BLOCKS); 1267 | let events = Parser::new_ext(input, opts); 1268 | 1269 | let mut output = String::new(); 1270 | let state = cmark(events, &mut output).unwrap(); 1271 | state.finalize(&mut output).unwrap(); 1272 | 1273 | assert_eq!(input, output); 1274 | } 1275 | 1276 | #[test] 1277 | fn toml_frontmatter_should_be_supported() { 1278 | let input = "+++ 1279 | key = value1 1280 | key = value2 1281 | +++ 1282 | 1283 | # Frontmatter should be supported"; 1284 | 1285 | let mut opts = Options::empty(); 1286 | opts.insert(Options::ENABLE_PLUSES_DELIMITED_METADATA_BLOCKS); 1287 | 1288 | let events = Parser::new_ext(input, opts); 1289 | let mut output = String::new(); 1290 | let state = cmark(events, &mut output).unwrap(); 1291 | state.finalize(&mut output).unwrap(); 1292 | 1293 | assert_eq!(input, output); 1294 | } 1295 | 1296 | #[test] 1297 | fn yaml_frontmatter_supports_newline_option() { 1298 | let mut newlines = String::new(); 1299 | 1300 | for i in 0..10 { 1301 | let input = format!( 1302 | "--- 1303 | key: value1 1304 | key: value2 1305 | ---{newlines} 1306 | # Frontmatter should be supported" 1307 | ); 1308 | 1309 | let mut opts = Options::empty(); 1310 | opts.insert(Options::ENABLE_YAML_STYLE_METADATA_BLOCKS); 1311 | 1312 | let events = Parser::new_ext(&input, opts); 1313 | let mut output = String::new(); 1314 | let state = cmark_with_options( 1315 | events, 1316 | &mut output, 1317 | pulldown_cmark_to_cmark::Options { 1318 | newlines_after_metadata: i, 1319 | ..Default::default() 1320 | }, 1321 | ) 1322 | .unwrap(); 1323 | state.finalize(&mut output).unwrap(); 1324 | 1325 | assert_eq!(input, output); 1326 | newlines.push('\n'); 1327 | } 1328 | } 1329 | 1330 | #[test] 1331 | fn toml_frontmatter_supports_newline_option() { 1332 | let mut newlines = String::new(); 1333 | 1334 | for i in 0..10 { 1335 | let input = format!( 1336 | "+++ 1337 | key = value1 1338 | key = value2 1339 | +++{newlines} 1340 | # Frontmatter should be supported" 1341 | ); 1342 | 1343 | let mut opts = Options::empty(); 1344 | opts.insert(Options::ENABLE_PLUSES_DELIMITED_METADATA_BLOCKS); 1345 | 1346 | let events = Parser::new_ext(&input, opts); 1347 | let mut output = String::new(); 1348 | let state = cmark_with_options( 1349 | events, 1350 | &mut output, 1351 | pulldown_cmark_to_cmark::Options { 1352 | newlines_after_metadata: i, 1353 | ..Default::default() 1354 | }, 1355 | ) 1356 | .unwrap(); 1357 | state.finalize(&mut output).unwrap(); 1358 | 1359 | assert_eq!(input, output); 1360 | newlines.push('\n'); 1361 | } 1362 | } 1363 | } 1364 | 1365 | mod definition_list { 1366 | use super::assert_events_eq; 1367 | 1368 | #[test] 1369 | fn round_trip() { 1370 | let input = r"First Term 1371 | : This is the definition of the first term. 1372 | 1373 | Second Term 1374 | : This is one definition of the second term. 1375 | : This is another definition of the second term."; 1376 | 1377 | assert_events_eq(input); 1378 | } 1379 | } 1380 | 1381 | mod source_range { 1382 | // Copied from `fmt.rs`. 1383 | 1384 | use pulldown_cmark::{utils::TextMergeStream, Options, Parser}; 1385 | use pulldown_cmark_to_cmark::{ 1386 | cmark_resume_with_source_range_and_options, cmark_with_source_range, Options as CmarkToCmarkOptions, State, 1387 | }; 1388 | 1389 | pub fn fmts(s: &str) -> (String, State<'_>) { 1390 | let mut buf = String::new(); 1391 | let mut s = cmark_with_source_range( 1392 | Parser::new_ext(s, Options::all()) 1393 | .into_offset_iter() 1394 | .map(|(e, r)| (e, Some(r))), 1395 | s, 1396 | &mut buf, 1397 | ) 1398 | .unwrap(); 1399 | // Not testing this field. 1400 | s.last_event_end_index = Default::default(); 1401 | (buf, s) 1402 | } 1403 | 1404 | pub fn fmts_with_options<'a>(s: &'a str, options: CmarkToCmarkOptions<'a>) -> (String, State<'a>) { 1405 | let mut buf = String::new(); 1406 | let mut s = cmark_resume_with_source_range_and_options( 1407 | Parser::new_ext(s, Options::all()) 1408 | .into_offset_iter() 1409 | .map(|(e, r)| (e, Some(r))), 1410 | s, 1411 | &mut buf, 1412 | None, 1413 | options, 1414 | ) 1415 | .unwrap(); 1416 | // Not testing this field. 1417 | s.last_event_end_index = Default::default(); 1418 | (buf, s) 1419 | } 1420 | 1421 | /// Asserts that if we parse our `str` s into a series of events, then serialize them with `cmark` 1422 | /// that we'll get the same series of events when we parse them again. 1423 | pub fn assert_events_eq(s: &str) { 1424 | let mut buf = String::new(); 1425 | cmark_with_source_range( 1426 | Parser::new_ext(s, Options::all()) 1427 | .into_offset_iter() 1428 | .map(|(e, r)| (e, Some(r))), 1429 | s, 1430 | &mut buf, 1431 | ) 1432 | .unwrap(); 1433 | 1434 | let before_events = TextMergeStream::new(Parser::new_ext(s, Options::all())); 1435 | let after_events = TextMergeStream::new(Parser::new_ext(&buf, Options::all())); 1436 | assert_eq!(before_events.collect::>(), after_events.collect::>()); 1437 | } 1438 | } 1439 | -------------------------------------------------------------------------------- /tests/integrate/main.rs: -------------------------------------------------------------------------------- 1 | mod display; 2 | mod fmt; 3 | mod spec; 4 | 5 | #[cfg(test)] 6 | mod fuzzed { 7 | use pulldown_cmark::{Event, HeadingLevel, Tag, TagEnd}; 8 | use pulldown_cmark_to_cmark::{cmark, Error}; 9 | 10 | #[test] 11 | fn cmark_with_invalid_event_stream() { 12 | let events = [ 13 | Event::Start(Tag::Heading { 14 | level: HeadingLevel::H2, 15 | id: None, 16 | classes: vec![], 17 | attrs: vec![], 18 | }), 19 | Event::Start(Tag::Heading { 20 | level: HeadingLevel::H2, 21 | id: None, 22 | classes: vec![], 23 | attrs: vec![], 24 | }), 25 | Event::Text(pulldown_cmark::CowStr::Borrowed("hello")), 26 | Event::End(TagEnd::Heading(HeadingLevel::H2)), 27 | Event::End(TagEnd::Heading(HeadingLevel::H2)), 28 | ]; 29 | assert!(matches!( 30 | cmark(events.iter(), String::new()), 31 | Err(Error::UnexpectedEvent) 32 | )); 33 | } 34 | } 35 | 36 | #[cfg(test)] 37 | mod calculate_code_block_token_count { 38 | use pulldown_cmark::{CodeBlockKind, CowStr, Event, Tag, TagEnd}; 39 | use pulldown_cmark_to_cmark::calculate_code_block_token_count; 40 | 41 | const CODE_BLOCK_START: Event<'_> = Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(CowStr::Borrowed("")))); 42 | const CODE_BLOCK_END: Event<'_> = Event::End(TagEnd::CodeBlock); 43 | 44 | #[test] 45 | fn no_token() { 46 | let events = &[CODE_BLOCK_START, Event::Text("text".into()), CODE_BLOCK_END]; 47 | assert_eq!(calculate_code_block_token_count(events.iter()), None); 48 | } 49 | 50 | #[test] 51 | fn backtick() { 52 | let events = &[CODE_BLOCK_START, Event::Text("```".into()), CODE_BLOCK_END]; 53 | assert_eq!(calculate_code_block_token_count(events.iter()), Some(4)); 54 | 55 | let events = &[CODE_BLOCK_START, Event::Text("````".into()), CODE_BLOCK_END]; 56 | assert_eq!(calculate_code_block_token_count(events.iter()), Some(5)); 57 | 58 | let events = &[CODE_BLOCK_START, Event::Text("``````````".into()), CODE_BLOCK_END]; 59 | assert_eq!(calculate_code_block_token_count(events.iter()), Some(11)); 60 | } 61 | 62 | #[test] 63 | fn tilde() { 64 | let events = &[CODE_BLOCK_START, Event::Text("~~~".into()), CODE_BLOCK_END]; 65 | assert_eq!(calculate_code_block_token_count(events.iter()), Some(4)); 66 | 67 | let events = &[CODE_BLOCK_START, Event::Text("~~~~".into()), CODE_BLOCK_END]; 68 | assert_eq!(calculate_code_block_token_count(events.iter()), Some(5)); 69 | 70 | let events = &[CODE_BLOCK_START, Event::Text("~~~~~~~~~~".into()), CODE_BLOCK_END]; 71 | assert_eq!(calculate_code_block_token_count(events.iter()), Some(11)); 72 | } 73 | 74 | #[test] 75 | fn mix() { 76 | let events = &[CODE_BLOCK_START, Event::Text("```~~~~".into()), CODE_BLOCK_END]; 77 | assert_eq!(calculate_code_block_token_count(events.iter()), Some(5)); 78 | 79 | let events = &[CODE_BLOCK_START, Event::Text("~~~~`````~~".into()), CODE_BLOCK_END]; 80 | assert_eq!(calculate_code_block_token_count(events.iter()), Some(6)); 81 | 82 | let events = &[ 83 | CODE_BLOCK_START, 84 | Event::Text("~~~```````~~~```~~".into()), 85 | CODE_BLOCK_END, 86 | ]; 87 | assert_eq!(calculate_code_block_token_count(events.iter()), Some(8)); 88 | } 89 | 90 | #[test] 91 | fn splitted_text() { 92 | let events = &[ 93 | CODE_BLOCK_START, 94 | Event::Text("~~~".into()), 95 | Event::Text("~~~".into()), 96 | CODE_BLOCK_END, 97 | ]; 98 | assert_eq!(calculate_code_block_token_count(events.iter()), Some(7)); 99 | 100 | let events = &[ 101 | CODE_BLOCK_START, 102 | Event::Text("````".into()), 103 | Event::Text("````".into()), 104 | CODE_BLOCK_END, 105 | ]; 106 | assert_eq!(calculate_code_block_token_count(events.iter()), Some(9)); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /tests/integrate/spec.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Ordering; 2 | use std::{convert::TryFrom, ops::Range}; 3 | 4 | use pretty_assertions::Comparison as PrettyComparison; 5 | use yansi::Paint; 6 | 7 | use pulldown_cmark::utils::TextMergeStream; 8 | use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag, TagEnd}; 9 | use pulldown_cmark_to_cmark::cmark; 10 | 11 | const COMMONMARK_SPEC_TEXT: &str = include_str!("../spec/CommonMark/spec.txt"); 12 | 13 | const COMMONMARK_SPEC_EXAMPLE_COUNT: usize = 649; 14 | 15 | // At the time of writing, ~90% of tests pass. This needs some additional work. 16 | const EXPECTED_SUCCESS_EXAMPLE_COUNT: usize = 585; 17 | 18 | const FULL_CMARK_RESULTS_VAR: &str = "FULL_CMARK_RESULTS"; 19 | 20 | struct MarkdownTestCase { 21 | markdown: String, 22 | expected_html: String, 23 | line_number: usize, 24 | } 25 | 26 | fn is_example_fence(tag: &Tag<'_>) -> bool { 27 | if let Tag::CodeBlock(CodeBlockKind::Fenced(fence_value)) = tag { 28 | &**fence_value == "example" 29 | } else { 30 | false 31 | } 32 | } 33 | 34 | fn collect_test_case<'a>(events: &mut impl Iterator, Range)>) -> Option<(String, String)> { 35 | let Event::Start(begin_tag) = events.next()?.0 else { 36 | return None; 37 | }; 38 | let Event::Text(text) = events.next()?.0 else { 39 | return None; 40 | }; 41 | let Event::End(end_tag) = events.next()?.0 else { 42 | return None; 43 | }; 44 | if !(is_example_fence(&begin_tag) && end_tag == TagEnd::CodeBlock) { 45 | return None; 46 | } 47 | let splitted_text = text.split("\n.\n").collect::>(); 48 | let Ok([input, output]) = <[_; 2]>::try_from(splitted_text) else { 49 | panic!("CommonMark spec example code block has unexpected form."); 50 | }; 51 | let output = output.trim_end_matches('\n'); 52 | Some((input.to_string(), output.to_string())) 53 | } 54 | 55 | fn parse_common_mark_testsuite() -> Vec { 56 | let opts = Options::empty(); 57 | let p = Parser::new_ext(COMMONMARK_SPEC_TEXT, opts).into_offset_iter(); 58 | 59 | let mut testsuite = vec![]; 60 | let mut p = p.peekable(); 61 | while let Some((peeked_event, range)) = p.peek() { 62 | match peeked_event { 63 | Event::Start(tag) if is_example_fence(tag) => (), 64 | _ => { 65 | let _ = p.next(); 66 | continue; 67 | } 68 | } 69 | 70 | let line_number = COMMONMARK_SPEC_TEXT[..range.start].lines().count() + 1; 71 | 72 | // a new example, insert it into the testsuite. 73 | let (markdown, expected_html) = collect_test_case(&mut p).expect("Error parsing example text from spec."); 74 | testsuite.push(MarkdownTestCase { 75 | line_number, 76 | markdown, 77 | expected_html, 78 | }); 79 | } 80 | 81 | testsuite 82 | } 83 | 84 | fn test_roundtrip(original: &str, expected_html: &str, line_number: usize, show_full_results: bool) -> bool { 85 | // 86 | // Markdown => [Event, ..] => Markdown 87 | // |_________ A _________| 88 | // |__________ B ________| 89 | // 90 | // A: pulldown-cmark 91 | // B: pulldown-cmark-to-cmark 92 | 93 | // Do A 94 | let opts = Options::empty(); 95 | let event_list = Parser::new_ext(original, opts).collect::>(); 96 | 97 | // Do B 98 | let mut regen_str = String::new(); 99 | cmark(event_list.iter().cloned(), &mut regen_str).expect("Regeneration failure"); 100 | 101 | // text events should be merged before comparing two event lists for equivalence. 102 | // you don't need to merge them before feeding them into `cmark`. 103 | let event_list: Vec> = TextMergeStream::new(event_list.into_iter()).collect(); 104 | let event_list_2 = TextMergeStream::new(Parser::new_ext(®en_str, opts)).collect::>(); 105 | 106 | if event_list == event_list_2 { 107 | return true; 108 | } 109 | 110 | if show_full_results { 111 | eprintln!( 112 | "{}\n", 113 | format!("===== Conformance Test Failure (L{line_number}) =====") 114 | .bold() 115 | .underline() 116 | ); 117 | 118 | eprintln!("{}\n", "Original Markdown Example"); 119 | eprint_indented(original, " "); 120 | eprintln!(); 121 | 122 | eprintln!("{}\n", "Regenerated Markdown Example"); 123 | eprint_indented(®en_str, " "); 124 | eprintln!(); 125 | 126 | eprintln!("{}\n", "Expected HTML"); 127 | eprint_indented(expected_html, " "); 128 | eprintln!(); 129 | 130 | eprintln!("{}\n", "Original vs Regenerated Event Sequence"); 131 | let comparision = PrettyComparison::new(&event_list, &event_list_2); 132 | for line in format!("{comparision}").lines() { 133 | eprintln!(" {}", line); 134 | } 135 | eprintln!(); 136 | } 137 | 138 | false 139 | } 140 | 141 | fn eprint_indented(text_block: &str, indent: &str) { 142 | for line in text_block.lines() { 143 | eprintln!("{indent}{line}"); 144 | } 145 | } 146 | 147 | //====================================== 148 | // Tests 149 | //====================================== 150 | 151 | #[test] 152 | fn commonmark_spec() { 153 | let testsuite = parse_common_mark_testsuite(); 154 | assert_eq!(COMMONMARK_SPEC_EXAMPLE_COUNT, testsuite.len()); 155 | 156 | let show_full_results = std::env::var(FULL_CMARK_RESULTS_VAR).is_ok(); 157 | 158 | let mut success_count = 0usize; 159 | for test_case in &testsuite { 160 | let MarkdownTestCase { 161 | markdown, 162 | expected_html, 163 | line_number, 164 | } = test_case; 165 | 166 | if test_roundtrip(markdown, expected_html, *line_number, show_full_results) { 167 | success_count += 1; 168 | } 169 | } 170 | 171 | let expected_percent = EXPECTED_SUCCESS_EXAMPLE_COUNT as f64 / testsuite.len() as f64; 172 | let actual_percent = success_count as f64 / testsuite.len() as f64; 173 | 174 | eprintln!(); 175 | 176 | let (change, change_icon) = match success_count.cmp(&EXPECTED_SUCCESS_EXAMPLE_COUNT) { 177 | // If the user requested the full results, then proceed to printing 178 | // the full results and failing the test, even if the test would 179 | // have otherwise passed if the user hadn't requested the results. 180 | Ordering::Equal if !show_full_results => return, 181 | Ordering::Equal => ("Unchanged".blue(), ""), 182 | Ordering::Less => ("DECREASED".red(), "🔻"), 183 | Ordering::Greater => ("INCREASED".green(), "🟢"), 184 | }; 185 | 186 | eprintln!("{}: {change}\n", "CommonMark Conformance Test Rate".bold().underline()); 187 | 188 | eprintln!( 189 | "Expected to pass: {} ({:.1}%)", 190 | EXPECTED_SUCCESS_EXAMPLE_COUNT, 191 | 100. * expected_percent 192 | ); 193 | eprintln!( 194 | " Actually passed: {success_count} ({:.1}%) {change_icon}", 195 | 100. * actual_percent, 196 | ); 197 | eprintln!(); 198 | eprintln!("CommonMark total: {}", testsuite.len()); 199 | eprintln!(); 200 | 201 | // Only ask the user to update the expected success count if they've managed 202 | // to increase it. Note: Some increases could be do to improvements in 203 | // pulldown-cmark, not this crate. 204 | if success_count > EXPECTED_SUCCESS_EXAMPLE_COUNT { 205 | eprintln!("Please update `EXPECTED_SUCCESS_EXAMPLE_COUNT` in {}\n", file!()); 206 | } 207 | 208 | eprintln!("To see the full results:\n"); 209 | eprintln!(" $ {}=true cargo test\n", FULL_CMARK_RESULTS_VAR); 210 | 211 | panic!() 212 | } 213 | -------------------------------------------------------------------------------- /tests/spec/CommonMark/LICENSE: -------------------------------------------------------------------------------- 1 | The CommonMark spec (spec.txt) and DTD (CommonMark.dtd) are 2 | 3 | Copyright (C) 2014-16 John MacFarlane 4 | 5 | Released under the Creative Commons CC-BY-SA 4.0 license: 6 | . 7 | 8 | Creative Commons Attribution-ShareAlike 4.0 International Public License 9 | 10 | By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-ShareAlike 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. 11 | 12 | Section 1 – Definitions. 13 | 14 | Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. 15 | Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. 16 | BY-SA Compatible License means a license listed at creativecommons.org/compatiblelicenses, approved by Creative Commons as essentially the equivalent of this Public License. 17 | Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. 18 | Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. 19 | Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. 20 | License Elements means the license attributes listed in the name of a Creative Commons Public License. The License Elements of this Public License are Attribution and ShareAlike. 21 | Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License. 22 | Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. 23 | Licensor means the individual(s) or entity(ies) granting rights under this Public License. 24 | Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. 25 | Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. 26 | You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. 27 | Section 2 – Scope. 28 | 29 | License grant. 30 | Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: 31 | reproduce and Share the Licensed Material, in whole or in part; and 32 | produce, reproduce, and Share Adapted Material. 33 | Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. 34 | Term. The term of this Public License is specified in Section 6(a). 35 | Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material. 36 | Downstream recipients. 37 | Offer from the Licensor – Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. 38 | Additional offer from the Licensor – Adapted Material. Every recipient of Adapted Material from You automatically receives an offer from the Licensor to exercise the Licensed Rights in the Adapted Material under the conditions of the Adapter’s License You apply. 39 | No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. 40 | No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). 41 | Other rights. 42 | 43 | Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. 44 | Patent and trademark rights are not licensed under this Public License. 45 | To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties. 46 | Section 3 – License Conditions. 47 | 48 | Your exercise of the Licensed Rights is expressly made subject to the following conditions. 49 | 50 | Attribution. 51 | 52 | If You Share the Licensed Material (including in modified form), You must: 53 | 54 | retain the following if it is supplied by the Licensor with the Licensed Material: 55 | identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); 56 | a copyright notice; 57 | a notice that refers to this Public License; 58 | a notice that refers to the disclaimer of warranties; 59 | a URI or hyperlink to the Licensed Material to the extent reasonably practicable; 60 | indicate if You modified the Licensed Material and retain an indication of any previous modifications; and 61 | indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. 62 | You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. 63 | If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. 64 | ShareAlike. 65 | In addition to the conditions in Section 3(a), if You Share Adapted Material You produce, the following conditions also apply. 66 | 67 | The Adapter’s License You apply must be a Creative Commons license with the same License Elements, this version or later, or a BY-SA Compatible License. 68 | You must include the text of, or the URI or hyperlink to, the Adapter's License You apply. You may satisfy this condition in any reasonable manner based on the medium, means, and context in which You Share Adapted Material. 69 | You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, Adapted Material that restrict exercise of the rights granted under the Adapter's License You apply. 70 | Section 4 – Sui Generis Database Rights. 71 | 72 | Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: 73 | 74 | for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database; 75 | if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material, including for purposes of Section 3(b); and 76 | You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. 77 | For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. 78 | Section 5 – Disclaimer of Warranties and Limitation of Liability. 79 | 80 | Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You. 81 | To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You. 82 | The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. 83 | Section 6 – Term and Termination. 84 | 85 | This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. 86 | Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: 87 | 88 | automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or 89 | upon express reinstatement by the Licensor. 90 | For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. 91 | For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. 92 | Sections 1, 5, 6, 7, and 8 survive termination of this Public License. 93 | Section 7 – Other Terms and Conditions. 94 | 95 | The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. 96 | Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. 97 | Section 8 – Interpretation. 98 | 99 | For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. 100 | To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. 101 | No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. 102 | Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. 103 | Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” The text of the Creative Commons public licenses is dedicated to the public domain under the CC0 Public Domain Dedication. Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at creativecommons.org/policies, Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. 104 | 105 | Creative Commons may be contacted at creativecommons.org. -------------------------------------------------------------------------------- /tests/spec/CommonMark/README.google: -------------------------------------------------------------------------------- 1 | URL: https://github.com/jgm/CommonMark.git 2 | Version: 1ef46a73e61968a60cc7b2e700381d719165b86c 3 | License: Creative Commons CC-BY-SA 4.0 4 | License File: LICENSE 5 | 6 | Description: 7 | CommonMark spec 8 | 9 | Local Modifications: 10 | This directory contains only the spec file. License file has been 11 | subsetted to only the files it applies to, and text of CC-BY-SA 4.0 12 | license has been added. 13 | -------------------------------------------------------------------------------- /tests/spec/CommonMark/smart_punct.txt: -------------------------------------------------------------------------------- 1 | ## Smart punctuation 2 | 3 | Open quotes are matched with closed quotes. 4 | The same method is used for matching openers and closers 5 | as is used in emphasis parsing: 6 | 7 | ```````````````````````````````` example_smartpunct 8 | "Hello," said the spider. 9 | "'Shelob' is my name." 10 | . 11 |

“Hello,” said the spider. 12 | “‘Shelob’ is my name.”

13 | ```````````````````````````````` 14 | 15 | ```````````````````````````````` example_smartpunct 16 | 'A', 'B', and 'C' are letters. 17 | . 18 |

‘A’, ‘B’, and ‘C’ are letters.

19 | ```````````````````````````````` 20 | 21 | ```````````````````````````````` example_smartpunct 22 | 'Oak,' 'elm,' and 'beech' are names of trees. 23 | So is 'pine.' 24 | . 25 |

‘Oak,’ ‘elm,’ and ‘beech’ are names of trees. 26 | So is ‘pine.’

27 | ```````````````````````````````` 28 | 29 | ```````````````````````````````` example_smartpunct 30 | 'He said, "I want to go."' 31 | . 32 |

‘He said, “I want to go.”’

33 | ```````````````````````````````` 34 | 35 | A single quote that isn't an open quote matched 36 | with a close quote will be treated as an 37 | apostrophe: 38 | 39 | ```````````````````````````````` example_smartpunct 40 | Were you alive in the 70's? 41 | . 42 |

Were you alive in the 70’s?

43 | ```````````````````````````````` 44 | 45 | ```````````````````````````````` example_smartpunct 46 | Here is some quoted '`code`' and a "[quoted link](url)". 47 | . 48 |

Here is some quoted ‘code’ and a “quoted link”.

49 | ```````````````````````````````` 50 | 51 | Here the first `'` is treated as an apostrophe, not 52 | an open quote, because the final single quote is matched 53 | by the single quote before `jolly`: 54 | 55 | ```````````````````````````````` example_smartpunct 56 | 'tis the season to be 'jolly' 57 | . 58 |

’tis the season to be ‘jolly’

59 | ```````````````````````````````` 60 | 61 | Multiple apostrophes should not be marked as open/closing quotes. 62 | 63 | ```````````````````````````````` example_smartpunct 64 | 'We'll use Jane's boat and John's truck,' Jenna said. 65 | . 66 |

‘We’ll use Jane’s boat and John’s truck,’ Jenna said.

67 | ```````````````````````````````` 68 | 69 | An unmatched double quote will be interpreted as a 70 | left double quote, to facilitate this style: 71 | 72 | ```````````````````````````````` example_smartpunct 73 | "A paragraph with no closing quote. 74 | 75 | "Second paragraph by same speaker, in fiction." 76 | . 77 |

“A paragraph with no closing quote.

78 |

“Second paragraph by same speaker, in fiction.”

79 | ```````````````````````````````` 80 | 81 | A quote following a `]` or `)` character cannot 82 | be an open quote: 83 | 84 | ```````````````````````````````` example_smartpunct 85 | [a]'s b' 86 | . 87 |

[a]’s b’

88 | ```````````````````````````````` 89 | 90 | Quotes that are escaped come out as literal straight 91 | quotes: 92 | 93 | ```````````````````````````````` example_smartpunct 94 | \"This is not smart.\" 95 | This isn\'t either. 96 | 5\'8\" 97 | . 98 |

"This is not smart." 99 | This isn't either. 100 | 5'8"

101 | ```````````````````````````````` 102 | 103 | Two hyphens form an en-dash, three an em-dash. 104 | 105 | ```````````````````````````````` example_smartpunct 106 | Some dashes: em---em 107 | en--en 108 | em --- em 109 | en -- en 110 | 2--3 111 | . 112 |

Some dashes: em—em 113 | en–en 114 | em — em 115 | en – en 116 | 2–3

117 | ```````````````````````````````` 118 | 119 | A sequence of more than three hyphens is 120 | parsed as a sequence of em and/or en dashes, 121 | with no hyphens. If possible, a homogeneous 122 | sequence of dashes is used (so, 10 hyphens 123 | = 5 en dashes, and 9 hyphens = 3 em dashes). 124 | When a heterogeneous sequence must be used, 125 | the em dashes come first, followed by the en 126 | dashes, and as few en dashes as possible are 127 | used (so, 7 hyphens = 2 em dashes an 1 en 128 | dash). 129 | 130 | ```````````````````````````````` example_smartpunct 131 | one- 132 | two-- 133 | three--- 134 | four---- 135 | five----- 136 | six------ 137 | seven------- 138 | eight-------- 139 | nine--------- 140 | thirteen-------------. 141 | . 142 |

one- 143 | two– 144 | three— 145 | four–– 146 | five—– 147 | six—— 148 | seven—–– 149 | eight–––– 150 | nine——— 151 | thirteen———––.

152 | ```````````````````````````````` 153 | 154 | Hyphens can be escaped: 155 | 156 | ```````````````````````````````` example_smartpunct 157 | Escaped hyphens: \-- \-\-\-. 158 | . 159 |

Escaped hyphens: -- ---.

160 | ```````````````````````````````` 161 | 162 | Three periods form an ellipsis: 163 | 164 | ```````````````````````````````` example_smartpunct 165 | Ellipses...and...and.... 166 | . 167 |

Ellipses…and…and….

168 | ```````````````````````````````` 169 | 170 | Periods can be escaped if ellipsis-formation 171 | is not wanted: 172 | 173 | ```````````````````````````````` example_smartpunct 174 | No ellipses\.\.\. 175 | . 176 |

No ellipses...

177 | ```````````````````````````````` 178 | -------------------------------------------------------------------------------- /tests/utilities.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | WHITE="$(tput setaf 9 || echo -n '')" 4 | YELLOW="$(tput setaf 3 || echo -n '')" 5 | GREEN="$(tput setaf 2 || echo -n '')" 6 | RED="$(tput setaf 1 || echo -n '')" 7 | OFFSET=( ) 8 | STEP=" " 9 | 10 | function title () { 11 | echo "$WHITE-----------------------------------------------------" 12 | echo "${GREEN}$*" 13 | echo "$WHITE-----------------------------------------------------" 14 | } 15 | 16 | function _context () { 17 | local name="${1:?}" 18 | shift 19 | echo 1>&2 "${YELLOW}${OFFSET[*]:-}[$name] $*" 20 | OFFSET+=("$STEP") 21 | } 22 | 23 | function with () { 24 | _context with "$*" 25 | } 26 | 27 | function when () { 28 | _context when "$*" 29 | } 30 | 31 | function _note () { 32 | local name="${1:?}" 33 | local color="${2:-}" 34 | shift 2 35 | echo 1>&2 -n "${OFFSET[*]:-}${color}[$name] ${*// /}" 36 | } 37 | 38 | function it () { 39 | _note it "${GREEN}" "$*" 40 | } 41 | 42 | function precondition () { 43 | _note precondition "${WHITE}" "$*" 44 | } 45 | 46 | function shortcoming () { 47 | _note shortcoming "${RED}" "$*" 48 | } 49 | 50 | function fail () { 51 | echo 1>&2 "${RED} $*" 52 | exit 1 53 | } 54 | 55 | function sandbox () { 56 | sandbox_tempdir="$(mktemp -t sandbox.XXXX -d)" 57 | # shellcheck disable=2064 58 | trap "popd >/dev/null" EXIT 59 | pushd "$sandbox_tempdir" >/dev/null \ 60 | || fail "Could not change directory into temporary directory." 61 | 62 | local custom_init="${1:-}" 63 | if [ -n "$custom_init" ]; then 64 | eval "$custom_init" 65 | fi 66 | } 67 | 68 | function expect_equals () { 69 | expect_run 0 test "${1:?}" = "${2:?}" 70 | } 71 | 72 | function expect_exists () { 73 | expect_run 0 test -e "${1:?}" 74 | } 75 | 76 | function expect_run_sh () { 77 | expect_run "${1:?}" bash -c "${2:?}" 78 | } 79 | 80 | function expect_snapshot () { 81 | local expected=${1:?} 82 | local actual=${2:?} 83 | if ! [ -e "$expected" ]; then 84 | cp -R "$actual" "$expected" 85 | fi 86 | expect_run 0 diff -r "$expected" "$actual" 87 | } 88 | 89 | function expect_run () { 90 | local expected_exit_code=$1 91 | shift 92 | local output= 93 | set +e 94 | output="$("$@" 2>&1)" 95 | 96 | local actual_exit_code=$? 97 | if [[ "$actual_exit_code" == "$expected_exit_code" ]]; then 98 | if [[ -n "${WITH_SNAPSHOT-}" ]]; then 99 | local expected="$WITH_SNAPSHOT" 100 | if ! [ -f "$expected" ]; then 101 | echo -n "$output" > "$expected" || exit 1 102 | fi 103 | if ! diff "$expected" <(echo -n "$output"); then 104 | echo 1>&2 "${RED} - FAIL" 105 | echo 1>&2 "${WHITE}\$$*" 106 | echo 1>&2 "Output snapshot did not match snapshot at '$expected'" 107 | echo 1>&2 "$output" 108 | exit 1 109 | fi 110 | fi 111 | echo 1>&2 112 | else 113 | echo 1>&2 "${RED} - FAIL" 114 | echo 1>&2 "${WHITE}\$$*" 115 | echo 1>&2 "${RED}Expected actual status $actual_exit_code to be $expected_exit_code" 116 | echo 1>&2 "$output" 117 | exit 1 118 | fi 119 | set -e 120 | } 121 | --------------------------------------------------------------------------------