├── .github └── workflows │ └── rust.yml ├── .gitignore ├── ARCHITECTURE.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── assets ├── example-v2.png ├── example-v3.png ├── example-v4.png └── example.png ├── egui_commonmark ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── examples │ ├── README.md │ ├── book.rs │ ├── cuddlyferris.png │ ├── hello_world.rs │ ├── html.rs │ ├── interactive.rs │ ├── link_hooks.rs │ ├── macros.rs │ ├── markdown │ │ ├── blockquotes.md │ │ ├── code-blocks.md │ │ ├── definition_list.md │ │ ├── embedded_image.md │ │ ├── headers.md │ │ ├── hello_world.md │ │ ├── html.md │ │ ├── lists.md │ │ └── tables.md │ ├── mixing.rs │ ├── scroll.rs │ └── show_mut.rs └── src │ ├── lib.rs │ └── parsers │ ├── mod.rs │ └── pulldown.rs ├── egui_commonmark_backend ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md └── src │ ├── alerts.rs │ ├── data_url_loader.rs │ ├── elements.rs │ ├── lib.rs │ ├── misc.rs │ └── pulldown.rs ├── egui_commonmark_macros ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── src │ ├── generator.rs │ └── lib.rs └── tests │ ├── fail │ ├── commonmark_str_not_found.rs │ ├── commonmark_str_not_found.stderr │ ├── incorrect_immutable.rs │ ├── incorrect_immutable.stderr │ ├── incorrect_type.rs │ └── incorrect_type.stderr │ ├── file.md │ └── pass │ ├── book.rs │ ├── commonmark.rs │ ├── commonmark_str.rs │ └── ui_hygiene.rs └── rust-toolchain /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Clippy 15 | run: cargo clippy -- -Dwarnings 16 | 17 | - name: Check all features pulldown 18 | run: cargo check --no-default-features --features pulldown_cmark,better_syntax_highlighting,macros,embedded_image 19 | 20 | - name: Cargo fmt 21 | run: cargo fmt --check -q 22 | 23 | - name: Test 24 | run: cargo test --features macros 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/target 2 | egui_commonmark_macros/wip/ 3 | -------------------------------------------------------------------------------- /ARCHITECTURE.md: -------------------------------------------------------------------------------- 1 | # Architecture 2 | 3 | ## Crates 4 | 5 | - `egui_commonmark` 6 | - `egui_commonmark_macros` 7 | - `egui_commonmark_backend` 8 | 9 | ### egui_commonmark 10 | 11 | This is the main crate. It depends on `egui_commonmark_backend` and can expose 12 | `egui_commonmark_macros` through the `macros` feature. 13 | 14 | ### egui_commonmark_macros 15 | 16 | This is a proc macro crate. It depends on `egui_commonmark_backend` 17 | 18 | ### egui_commonmark_backend 19 | 20 | This is a crate that contains all code shared between the egui_commonmark 21 | crates. The code in this crate is also used by the code generated by 22 | `egui_commonmark_macros`. As a result the visibility of most items in it is `pub` 23 | by default. Since a user should not directly rely on these APIs most elements 24 | are hidden from the documentation. 25 | 26 | `egui_commonmark` reexports some of it's items. So care must be taken to ensure 27 | that no implementation details are accidentally exposed. 28 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # egui_commomnark changelog 2 | 3 | ## Unreleased 4 | 5 | ### Changed 6 | 7 | - Updated to pulldown-cmark 0.13 8 | 9 | 10 | ### Fixed 11 | 12 | - Rendering of html in macros 13 | 14 | ## 0.20.0 - 2025-02-04 15 | 16 | ### Added 17 | 18 | - Callback function `render_math_fn` for custom math rendering 19 | - Callback function `render_html_fn` for custom html rendering 20 | 21 | ### Changed 22 | 23 | - Updated egui to 0.31 ([#71](https://github.com/lampsitter/egui_commonmark/pull/71) by 24 | [@Wumpf](https://github.com/Wumpf) and [@emilk](https://github.com/emilk)) 25 | 26 | ### Fixed 27 | 28 | - Html is rendered as text instead of not being displayed 29 | 30 | ## 0.19.0 - 2024-12-17 31 | 32 | ## Added 33 | 34 | - Support for loading images embedded in the markdown with data urls. 35 | 36 | ### Changed 37 | 38 | - Updated egui to 0.30 ([#69](https://github.com/lampsitter/egui_commonmark/pull/69) by [@abey79](https://github.com/abey79)) 39 | 40 | ## 0.18.0 - 2024-09-26 41 | 42 | ### Added 43 | 44 | - Definition lists 45 | - Proper inline code block rendering 46 | 47 | ### Changed 48 | 49 | - `CommonMarkViewer::new` no longer takes in an id. 50 | - `commonmark!` and `commonmark_str!` no longer takes in an id. 51 | - `CommonMarkViewer::show_scrollable` takes in an id explicity. 52 | 53 | - Updated pulldown-cmark to 0.12 54 | - Newlines are no longer inserted before/after markdown ([#56](https://github.com/lampsitter/egui_commonmark/pull/56)) 55 | > For the old behaviour you can call `ui.label("");` before and and after 56 | 57 | ### Removed 58 | 59 | - Experimental comrak backend ([#57](https://github.com/lampsitter/egui_commonmark/pull/57)) 60 | - Deprecated method `syntax_theme` 61 | 62 | ## 0.17.0 - 2024-07-03 63 | 64 | ### Changed 65 | 66 | - Updated egui to 0.28 ([#51](https://github.com/lampsitter/egui_commonmark/pull/51) by [@emilk](https://github.com/emilk)) 67 | - Updated pulldown-cmark to 0.11 68 | 69 | ## 0.16.1 - 2024-05-12 70 | 71 | ## Fixed 72 | 73 | - Fixed docs.rs build 74 | 75 | ## 0.16.0 - 2024-05-12 76 | 77 | ### Added 78 | 79 | - `commonmark!` and `commonmark_str!` for compile time parsing of markdown. The 80 | proc macros will output egui widgets directly into your code. To use this 81 | enable the `macros` feature. 82 | 83 | ### Changed 84 | 85 | - MSRV bumped to 1.76 86 | 87 | ### Fixed 88 | 89 | - Missing newline before alerts 90 | 91 | ## 0.15.0 - 2024-04-02 92 | 93 | ### Added 94 | 95 | - Replace copy icon with checkmark when clicking copy button in code blocks 96 | ([#42](https://github.com/lampsitter/egui_commonmark/pull/42) by [@zeozeozeo](https://github.com/zeozeozeo)) 97 | - Interactive tasklists with `CommonMarkViewer::show_mut` 98 | ([#40](https://github.com/lampsitter/egui_commonmark/pull/40) by [@crumblingstatue](https://github.com/crumblingstatue)) 99 | 100 | ### Changed 101 | 102 | - MSRV bumped to 1.74 due to pulldown_cmark 103 | - Alerts are case-insensitive 104 | - More spacing between list indicator and elements ([#46](https://github.com/lampsitter/egui_commonmark/pull/46)) 105 | 106 | ### Fixed 107 | 108 | - Lists align text when wrapping instead of wrapping at the beginning of the next 109 | line ([#46](https://github.com/lampsitter/egui_commonmark/pull/46)) 110 | - Code blocks won't insert a newline when in lists 111 | - In certain scenarios there was no newline after lists 112 | - Copy button for code blocks show the correct cursor again on hover (regression 113 | after egui 0.27) 114 | 115 | ## 0.14.0 - 2024-03-26 116 | 117 | ### Added 118 | 119 | - `AlertBundle::from_alerts` 120 | - `AlertBundle::into_alerts` 121 | 122 | ### Changed 123 | 124 | - Update to egui 0.27 ([#37](https://github.com/lampsitter/egui_commonmark/pull/37) by [@emilk](https://github.com/emilk)) 125 | - `CommonMarkViewer::show` returns `InnerResponse<()>` 126 | ([#36](https://github.com/lampsitter/egui_commonmark/pull/36) by [@ElhamAryanpur](https://github.com/ElhamAryanpur)) 127 | 128 | ### Fixed 129 | 130 | - A single table cell split into multiple cells ([#35](https://github.com/lampsitter/egui_commonmark/pull/35)) 131 | 132 | ## 0.13.0 - 2024-02-20 133 | 134 | ### Added 135 | 136 | - Alerts ([#32](https://github.com/lampsitter/egui_commonmark/pull/32)) 137 | 138 | > [!TIP] 139 | > Alerts like this can be used 140 | 141 | ### Changed 142 | 143 | - Prettier blockquotes 144 | 145 | Before two simple horizontal lines were rendered. Now it's a single horizontal 146 | line in front of the elements. 147 | 148 | - Upgraded to pulldown-cmark 0.10 149 | 150 | ### Fixed 151 | 152 | - Ordered lists remember their number when mixing bullet and ordered lists 153 | 154 | ## 0.12.1 - 2024-02-12 155 | 156 | ### Fixed 157 | 158 | - Build failure with 1.72 159 | 160 | 161 | ## 0.12.0 - 2024-02-05 162 | 163 | ### Changed 164 | 165 | - Update to egui 0.26 166 | 167 | ### Fixed 168 | 169 | - Missing space after tables 170 | 171 | 172 | ## 0.11.0 - 2024-01-08 173 | 174 | ### Changed 175 | 176 | - Update to egui 0.25 ([#27](https://github.com/lampsitter/egui_commonmark/pull/27) by [@emilk](https://github.com/emilk)) 177 | 178 | 179 | ## 0.10.2 - 2023-12-13 180 | 181 | ### Added 182 | 183 | - Option to change default implicit uri scheme [#24](https://github.com/lampsitter/egui_commonmark/pull/24) 184 | 185 | ## 0.10.1 - 2023-12-03 186 | 187 | ### Changed 188 | 189 | - Code block has borders. 190 | 191 | ### Fixed 192 | 193 | - Make code blocks non-editable ([#22](https://github.com/lampsitter/egui_commonmark/pull/22) by [@emilk](https://github.com/emilk)). 194 | 195 | 196 | ## 0.10.0 - 2023-11-23 197 | 198 | ### Changed 199 | 200 | - Update to egui 0.24 201 | 202 | ## 0.9.2 - 2023-11-07 203 | 204 | ### Fixed 205 | 206 | - Header sizing issues ([#20](https://github.com/lampsitter/egui_commonmark/pull/20) by [@abey79](https://github.com/abey79)). 207 | 208 | ## 0.9.1 - 2023-10-24 209 | 210 | ### Fixed 211 | 212 | - Missing space after heading when preceded by an image 213 | - Missing space after separator 214 | 215 | ## 0.9.0 - 2023-10-14 216 | 217 | ### Added 218 | 219 | - Copy text button in code blocks 220 | 221 | ## 0.8.0 - 2023-09-28 222 | 223 | ### Added 224 | 225 | - Primitive syntax highlighting by default 226 | - Code blocks now use the syntax highlighting theme's caret and selection colors while using the 227 | `better_syntax_highlighting` feature. 228 | - Image loading errors are shown ([#8](https://github.com/lampsitter/egui_commonmark/pull/8) by [@emilk](https://github.com/emilk)). 229 | - `CommonMarkCache` implements `Debug` ([#7](https://github.com/lampsitter/egui_commonmark/pull/7) by [@ChristopherPerry6060](https://github.com/ChristopherPerry6060)). 230 | - `CommonMarkCache::add_syntax_themes_from_folder` 231 | - `CommonMarkCache::add_syntax_theme_from_bytes` 232 | - `CommonMarkViewer::explicit_image_uri_scheme` 233 | 234 | ### Fixed 235 | 236 | - Links of the type ``[`fancy` _link_](..)`` is rendered correctly. 237 | 238 | ### Changed 239 | 240 | - Update to egui 0.23 241 | - Image formats are no longer implicitly enabled. 242 | - Use new image API from egui ([#11](https://github.com/lampsitter/egui_commonmark/pull/11) by [@jprochazk](https://github.com/jprochazk)). 243 | - Feature `syntax_highlighting` has been renamed to `better_syntax_highlighting`. 244 | 245 | ### Removed 246 | 247 | - `CommonMarkCache::reload_images` 248 | - Removed trimming of svg's transparency. The function has been removed from resvg. 249 | 250 | ## 0.7.4 - 2023-07-08 251 | 252 | ### Changed 253 | 254 | - Better looking checkboxes 255 | 256 | ## 0.7.3 - 2023-05-24 257 | 258 | ### Added 259 | 260 | - Support for egui 0.22. This release can also still be used with 0.21. 261 | An explicit dependency update might be needed to use egui 0.22: `cargo update -p egui_commonmark` 262 | 263 | ## 0.7.2 - 2023-04-22 264 | 265 | ### Added 266 | 267 | - `CommonMarkCache::clear_scrollable_with_id` to clear the cache for only a single scrollable viewer. 268 | 269 | ### Fixed 270 | 271 | - Removed added spacing between elements in `show_scrollable` 272 | 273 | ## 0.7.1 - 2023-04-21 274 | 275 | ### Added 276 | 277 | - Only render visible elements within a ScrollArea with `show_scrollable` 278 | ([#4](https://github.com/lampsitter/egui_commonmark/pull/4) by [@localcc](https://github.com/localcc)). 279 | 280 | ## 0.7.0 - 2023-02-09 281 | 282 | ### Changed 283 | 284 | - Upgraded egui to 0.21 285 | 286 | ## 0.6.0 - 2022-12-08 287 | 288 | ### Changed 289 | 290 | - Upgraded egui to 0.20 291 | 292 | ## 0.5.0 - 2022-11-29 293 | 294 | ### Changed 295 | 296 | - Default dark syntax highlighting theme has been changed from base16-mocha.dark 297 | to base16-ocean.dark. 298 | 299 | ### Fixed 300 | 301 | - Render text in svg images. 302 | - Fixed erroneous newline after images. 303 | - Fixed missing newline after lists and quotes. 304 | 305 | ## 0.4.0 - 2022-08-25 306 | 307 | ### Changed 308 | 309 | - Upgraded egui to 0.19. 310 | 311 | ### Fixed 312 | 313 | - Display indented code blocks in a single code block ([#1](https://github.com/lampsitter/egui_commonmark/pull/1) by [@lazytanuki](https://github.com/lazytanuki)). 314 | 315 | ## 0.3.0 - 2022-08-13 316 | 317 | ### Added 318 | 319 | - Automatic light/dark theme in code blocks. 320 | - Copyable code blocks. 321 | 322 | ### Changed 323 | 324 | - Deprecated `syntax_theme` in favour of `syntax_theme_dark` and 325 | `syntax_theme_light`. 326 | 327 | ### Fixed 328 | 329 | - No longer panic upon unknown syntax theme. 330 | - Fixed incorrect line endings within headings. 331 | 332 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## How to contribute 2 | 3 | First of all thank you for considering it. Check out 4 | [ARCHITECTURE.md](ARCHITECTURE.md) for an overview of how the repo is put together. 5 | 6 | ### Running tests 7 | 8 | You won't be able to run the tests without enabling the `macros` feature 9 | as one of the examples depend on it. 10 | 11 | `cargo test --features macros` 12 | 13 | 14 | ### Debugging the proc macros 15 | 16 | To see the output of the macros enable the `dump-macro` feature. 17 | For the macro example the output can be viewed like this: 18 | 19 | ```sh 20 | cargo r --example macro --features dump-macro,macros -- dark | rustfmt --edition=2021 | less 21 | ``` 22 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "egui_commonmark", 4 | "egui_commonmark_macros", 5 | "egui_commonmark_backend", 6 | ] 7 | 8 | resolver = "2" 9 | 10 | 11 | [workspace.package] 12 | license = "MIT OR Apache-2.0" 13 | edition = "2021" 14 | rust-version = "1.81" # Follows egui 15 | version = "0.20.0" 16 | repository = "https://github.com/lampsitter/egui_commonmark" 17 | 18 | [workspace.dependencies] 19 | egui_extras = { version = "0.31", default-features = false } 20 | egui = { version = "0.31", default-features = false } 21 | 22 | egui_commonmark_backend = { version = "0.20.0", path = "egui_commonmark_backend", default-features = false } 23 | egui_commonmark_macros = { version = "0.20.0", path = "egui_commonmark_macros", default-features = false } 24 | 25 | # To add features to documentation 26 | document-features = { version = "0.2" } 27 | 28 | pulldown-cmark = { version = "0.13", default-features = false } 29 | 30 | 31 | [patch.crates-io] 32 | # eframe = { git = "https://github.com/emilk/egui.git", branch = "master" } 33 | # egui = { git = "https://github.com/emilk/egui.git", branch = "master" } 34 | # egui_extras = { git = "https://github.com/emilk/egui.git", branch = "master" } 35 | 36 | # eframe = { path = "../../egui/crates/eframe" } 37 | # egui = { path = "../../egui/crates/egui" } 38 | # egui_extras = { path = "../../egui/crates/egui_extras" } 39 | -------------------------------------------------------------------------------- /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 [yyyy] [name of copyright owner] 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 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022-2025 Erlend Walstad 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A commonmark viewer for [egui](https://github.com/emilk/egui) 2 | 3 | [![Crate](https://img.shields.io/crates/v/egui_commonmark.svg)](https://crates.io/crates/egui_commonmark) 4 | [![Documentation](https://docs.rs/egui_commonmark/badge.svg)](https://docs.rs/egui_commonmark) 5 | 6 | showcase 7 | 8 | While this crate's main focus is commonmark, it also supports a subset of 9 | Github's markdown syntax: tables, strikethrough, tasklists and footnotes. 10 | 11 | ## Usage 12 | 13 | In Cargo.toml: 14 | 15 | ```toml 16 | egui_commonmark = "0.20" 17 | # Specify what image formats you want to use 18 | image = { version = "0.25", default-features = false, features = ["png"] } 19 | ``` 20 | 21 | ```rust 22 | use egui_commonmark::*; 23 | let markdown = 24 | r"# Hello world 25 | 26 | * A list 27 | * [ ] Checkbox 28 | "; 29 | 30 | let mut cache = CommonMarkCache::default(); 31 | CommonMarkViewer::new().show(ui, &mut cache, markdown); 32 | ``` 33 | 34 | 35 | ## Compile time evaluation of markdown 36 | 37 | If you want to embed markdown directly the binary then you can enable the `macros` feature. 38 | This will do the parsing of the markdown at compile time and output egui widgets. 39 | 40 | ### Example 41 | 42 | ```rust 43 | use egui_commonmark::{CommonMarkCache, commonmark}; 44 | let mut cache = CommonMarkCache::default(); 45 | let _response = commonmark!(ui, &mut cache, "# ATX Heading Level 1"); 46 | ``` 47 | 48 | Alternatively you can embed a file 49 | 50 | ### Example 51 | 52 | ```rust 53 | use egui_commonmark::{CommonMarkCache, commonmark_str}; 54 | let mut cache = CommonMarkCache::default(); 55 | commonmark_str!(ui, &mut cache, "content.md"); 56 | ``` 57 | 58 | 59 | ## Features 60 | 61 | * `macros`: macros for compile time parsing of markdown 62 | * `better_syntax_highlighting`: Syntax highlighting inside code blocks with 63 | [`syntect`](https://crates.io/crates/syntect) 64 | * `svg`: Support for viewing svg images 65 | * `fetch`: Images with urls will be downloaded and displayed 66 | 67 | 68 | ## Examples 69 | 70 | For an easy intro check out the `hello_world` example. To see all the different 71 | features egui_commonmark has to offer check out the `book` example. 72 | 73 | ## FAQ 74 | 75 | ### URL is not displayed when hovering over a link 76 | 77 | By default egui does not show urls when you hover hyperlinks. To enable it, 78 | you can do the following before calling any ui related functions: 79 | 80 | ```rust 81 | ui.style_mut().url_in_tooltip = true; 82 | ``` 83 | 84 | ## MSRV Policy 85 | 86 | This crate uses the same MSRV as the latest released egui version. 87 | 88 | ## License 89 | 90 | Licensed under either of 91 | 92 | * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 93 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 94 | 95 | at your option. 96 | -------------------------------------------------------------------------------- /assets/example-v2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lampsitter/egui_commonmark/86f6a1d4a1fbe38b89f0034de839d879fe540f31/assets/example-v2.png -------------------------------------------------------------------------------- /assets/example-v3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lampsitter/egui_commonmark/86f6a1d4a1fbe38b89f0034de839d879fe540f31/assets/example-v3.png -------------------------------------------------------------------------------- /assets/example-v4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lampsitter/egui_commonmark/86f6a1d4a1fbe38b89f0034de839d879fe540f31/assets/example-v4.png -------------------------------------------------------------------------------- /assets/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lampsitter/egui_commonmark/86f6a1d4a1fbe38b89f0034de839d879fe540f31/assets/example.png -------------------------------------------------------------------------------- /egui_commonmark/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "egui_commonmark" 3 | authors = ["Erlend Walstad"] 4 | 5 | version.workspace = true 6 | edition.workspace = true 7 | license.workspace = true 8 | rust-version.workspace = true 9 | repository.workspace = true 10 | 11 | description = "Commonmark viewer for egui" 12 | keywords = ["commonmark", "egui"] 13 | categories = ["gui"] 14 | readme = "README.md" 15 | documentation = "https://docs.rs/egui_commonmark" 16 | include = ["src/**/*.rs", "LICENSE-MIT", "LICENSE-APACHE", "Cargo.toml"] 17 | 18 | [dependencies] 19 | egui_commonmark_backend = { workspace = true } 20 | egui_commonmark_macros = { workspace = true, optional = true } 21 | 22 | egui_extras = { workspace = true } 23 | egui = { workspace = true } 24 | 25 | document-features = { workspace = true, optional = true } 26 | 27 | pulldown-cmark = { workspace = true } 28 | 29 | [features] 30 | default = ["load-images", "pulldown_cmark"] 31 | 32 | ## Enable proc macros for compile time generation of egui widgets from markdown 33 | macros = [ 34 | "dep:egui_commonmark_macros", 35 | ] # For simplicity it only supports pulldown-cmark 36 | 37 | ## Builds upon the `macros` feature. Enables tracking of markdown files to recompile 38 | ## when their content changes. Uses nightly features 39 | nightly = ["macros", "egui_commonmark_macros/nightly"] 40 | 41 | # For internal debugging use only! 42 | dump-macro = ["egui_commonmark_macros/dump-macro"] 43 | 44 | ## No-op feature 45 | pulldown_cmark = [] 46 | 47 | ## Syntax highlighting for code blocks using syntect 48 | better_syntax_highlighting = [ 49 | "egui_commonmark_backend/better_syntax_highlighting", 50 | ] 51 | 52 | ## Enable loading of images. Make sure to also opt in to what image format you need 53 | ## through the image crate. 54 | load-images = ["egui_extras/image", "egui_extras/file"] 55 | 56 | ## Support loading svg images 57 | svg = ["egui_extras/svg"] 58 | 59 | ## Images with urls will be downloaded and displayed 60 | fetch = ["egui_extras/http"] 61 | 62 | ## Allows loading base64 image data urls from within markdown files. e.g: `data:image/png;base64,...` 63 | ## Note that this is really space inefficient. No size limit is in place for the maximum allowed 64 | ## data in the url. 65 | ## 66 | ## This enables the data urls for your entire app as it installs an egui bytes loader 67 | ## in the background. 68 | ## 69 | ## Currently this does not support wasm. 70 | embedded_image = ["egui_commonmark_backend/embedded_image"] 71 | 72 | [dev-dependencies] 73 | eframe = { version = "0.31", default-features = false, features = [ 74 | "default_fonts", 75 | "glow", 76 | "wayland", 77 | "x11", 78 | ] } 79 | image = { version = "0.25", default-features = false, features = ["png"] } 80 | egui_commonmark_macros = { workspace = true } # Tests won't build otherswise 81 | 82 | [package.metadata.docs.rs] 83 | features = ["better_syntax_highlighting", "document-features", "macros"] 84 | -------------------------------------------------------------------------------- /egui_commonmark/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /egui_commonmark/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /egui_commonmark/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /egui_commonmark/examples/README.md: -------------------------------------------------------------------------------- 1 | # Overview of examples 2 | 3 | ## hello_world.rs 4 | 5 | A good starting point. 6 | 7 | ## interactive.rs 8 | 9 | An interactive example where you can play around with the markdown text at 10 | runtime. 11 | 12 | ## book.rs 13 | 14 | Intended to show all the different rendering features of the crate. 15 | 16 | ## macros.rs 17 | 18 | Features compile time markdown parsing. 19 | 20 | ## show_mut.rs 21 | 22 | How to make checkboxes interactive. 23 | 24 | ## link_hooks.rs 25 | 26 | Allow hijacking links for doing operations within the application such as 27 | changing a markdown page in a book without displaying the destination link. 28 | 29 | ## mixing.rs 30 | 31 | Shows commonmark elements mixed with egui widgets. It displays the widgets with 32 | no spaces in between as if the markdown was egui widgets. 33 | 34 | ## scroll.rs 35 | 36 | Intended to allow showing a long markdown text and only process the displayed 37 | parts. Currently it only works in very basic cases, the feature requires some 38 | more work to be generally useful. 39 | 40 | -------------------------------------------------------------------------------- /egui_commonmark/examples/book.rs: -------------------------------------------------------------------------------- 1 | //! Make sure to run this example from the repo directory and not the example 2 | //! directory. To see all the features in full effect, run this example with 3 | //! `cargo r --features better_syntax_highlighting,svg,fetch` 4 | //! Add `light` or `dark` to the end of the command to specify theme. Default 5 | //! is system theme. `cargo r --features better_syntax_highlighting,svg,fetch -- dark` 6 | //! 7 | //! Shows a simple way to use the crate to implement a book like view. 8 | 9 | use eframe::egui; 10 | use egui_commonmark::*; 11 | 12 | struct Page { 13 | name: String, 14 | content: String, 15 | } 16 | 17 | struct App { 18 | cache: CommonMarkCache, 19 | curr_tab: Option, 20 | pages: Vec, 21 | } 22 | 23 | impl App { 24 | fn sidepanel(&mut self, ui: &mut egui::Ui) { 25 | egui::SidePanel::left("left_documentation_panel") 26 | .resizable(false) 27 | .default_width(100.0) 28 | .show_inside(ui, |ui| { 29 | let style = ui.style_mut(); 30 | style.visuals.widgets.active.bg_stroke = egui::Stroke::NONE; 31 | style.visuals.widgets.hovered.bg_stroke = egui::Stroke::NONE; 32 | style.visuals.widgets.inactive.bg_fill = egui::Color32::TRANSPARENT; 33 | style.visuals.widgets.inactive.bg_stroke = egui::Stroke::NONE; 34 | ui.with_layout(egui::Layout::top_down_justified(egui::Align::LEFT), |ui| { 35 | for (i, p) in self.pages.iter().enumerate() { 36 | if Some(i) == self.curr_tab { 37 | let _ = ui.selectable_label(true, &p.name); 38 | } else if ui.selectable_label(false, &p.name).clicked() { 39 | self.curr_tab = Some(i); 40 | } 41 | ui.separator(); 42 | } 43 | }); 44 | }); 45 | } 46 | 47 | fn content_panel(&mut self, ui: &mut egui::Ui) { 48 | egui::ScrollArea::vertical().show(ui, |ui| { 49 | // Add a frame with margin to prevent the content from hugging the sidepanel 50 | egui::Frame::new() 51 | .inner_margin(egui::Margin::symmetric(5, 0)) 52 | .show(ui, |ui| { 53 | CommonMarkViewer::new() 54 | .default_width(Some(200)) 55 | .max_image_width(Some(512)) 56 | .show( 57 | ui, 58 | &mut self.cache, 59 | &self.pages[self.curr_tab.unwrap_or(0)].content, 60 | ); 61 | }); 62 | }); 63 | } 64 | } 65 | 66 | impl eframe::App for App { 67 | fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { 68 | egui::CentralPanel::default().show(ctx, |ui| { 69 | self.sidepanel(ui); 70 | self.content_panel(ui); 71 | }); 72 | } 73 | } 74 | 75 | fn main() -> eframe::Result { 76 | let mut args = std::env::args(); 77 | args.next(); 78 | 79 | eframe::run_native( 80 | "Markdown viewer", 81 | eframe::NativeOptions::default(), 82 | Box::new(move |cc| { 83 | if let Some(theme) = args.next() { 84 | if theme == "light" { 85 | cc.egui_ctx.set_theme(egui::Theme::Light); 86 | } else if theme == "dark" { 87 | cc.egui_ctx.set_theme(egui::Theme::Dark); 88 | } 89 | } 90 | cc.egui_ctx.style_mut(|style| { 91 | // Show the url of a hyperlink on hover 92 | style.url_in_tooltip = true; 93 | }); 94 | 95 | Ok(Box::new(App { 96 | cache: CommonMarkCache::default(), 97 | curr_tab: Some(0), 98 | pages: vec![ 99 | Page { 100 | name: "Hello World".to_owned(), 101 | content: include_str!("markdown/hello_world.md").to_owned(), 102 | }, 103 | Page { 104 | name: "Headers".to_owned(), 105 | content: include_str!("markdown/headers.md").to_owned(), 106 | }, 107 | Page { 108 | name: "Lists".to_owned(), 109 | content: include_str!("markdown/lists.md").to_owned(), 110 | }, 111 | Page { 112 | name: "Definition lists".to_owned(), 113 | content: include_str!("markdown/definition_list.md").to_owned(), 114 | }, 115 | Page { 116 | name: "Code blocks".to_owned(), 117 | content: include_str!("markdown/code-blocks.md").to_owned(), 118 | }, 119 | Page { 120 | name: "Block Quotes".to_owned(), 121 | content: include_str!("markdown/blockquotes.md").to_owned(), 122 | }, 123 | Page { 124 | name: "Tables".to_owned(), 125 | content: include_str!("markdown/tables.md").to_owned(), 126 | }, 127 | Page { 128 | name: "Embedded Image".to_owned(), 129 | content: include_str!("markdown/embedded_image.md").to_owned(), 130 | }, 131 | Page { 132 | name: "Html text".to_owned(), 133 | content: include_str!("markdown/html.md").to_owned(), 134 | }, 135 | ], 136 | })) 137 | }), 138 | ) 139 | } 140 | -------------------------------------------------------------------------------- /egui_commonmark/examples/cuddlyferris.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lampsitter/egui_commonmark/86f6a1d4a1fbe38b89f0034de839d879fe540f31/egui_commonmark/examples/cuddlyferris.png -------------------------------------------------------------------------------- /egui_commonmark/examples/hello_world.rs: -------------------------------------------------------------------------------- 1 | //! Make sure to run this example from the repo directory and not the example 2 | //! directory. To see all the features in full effect, run this example with 3 | //! `cargo r --features better_syntax_highlighting,svg,fetch` 4 | //! Add `light` or `dark` to the end of the command to specify theme. Default 5 | //! is system theme. `cargo r --features better_syntax_highlighting,svg,fetch -- dark` 6 | 7 | use eframe::egui; 8 | use egui_commonmark::*; 9 | 10 | struct App { 11 | cache: CommonMarkCache, 12 | } 13 | 14 | impl eframe::App for App { 15 | fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { 16 | let text = include_str!("markdown/hello_world.md"); 17 | egui::CentralPanel::default().show(ctx, |ui| { 18 | egui::ScrollArea::vertical().show(ui, |ui| { 19 | CommonMarkViewer::new() 20 | .max_image_width(Some(512)) 21 | .show(ui, &mut self.cache, text); 22 | }); 23 | }); 24 | } 25 | } 26 | 27 | fn main() -> eframe::Result { 28 | let mut args = std::env::args(); 29 | args.next(); 30 | 31 | eframe::run_native( 32 | "Markdown viewer", 33 | eframe::NativeOptions::default(), 34 | Box::new(move |cc| { 35 | if let Some(theme) = args.next() { 36 | if theme == "light" { 37 | cc.egui_ctx.set_theme(egui::Theme::Light); 38 | } else if theme == "dark" { 39 | cc.egui_ctx.set_theme(egui::Theme::Dark); 40 | } 41 | } 42 | 43 | cc.egui_ctx.style_mut(|style| { 44 | // Show the url of a hyperlink on hover 45 | style.url_in_tooltip = true; 46 | }); 47 | 48 | Ok(Box::new(App { 49 | cache: CommonMarkCache::default(), 50 | })) 51 | }), 52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /egui_commonmark/examples/html.rs: -------------------------------------------------------------------------------- 1 | //! Add `light` or `dark` to the end of the command to specify theme. Default 2 | //! is light. `cargo r --example html -- dark` 3 | 4 | use eframe::egui; 5 | use egui_commonmark::*; 6 | use std::cell::RefCell; 7 | use std::rc::Rc; 8 | 9 | struct App { 10 | cache: CommonMarkCache, 11 | /// To avoid id collisions 12 | counter: Rc>, 13 | } 14 | 15 | impl eframe::App for App { 16 | fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { 17 | *self.counter.as_ref().borrow_mut() = 0; 18 | 19 | egui::CentralPanel::default().show(ctx, |ui| { 20 | egui::ScrollArea::vertical().show(ui, |ui| { 21 | CommonMarkViewer::new() 22 | .render_html_fn({ 23 | let counter = Rc::clone(&self.counter); 24 | Some(&move |ui, html| { 25 | // For simplicity lets just hide the content regardless of what kind of 26 | // node it is. 27 | ui.collapsing( 28 | format!("Collapsed {}", counter.as_ref().borrow()), 29 | |ui| { 30 | ui.label(html); 31 | }, 32 | ); 33 | 34 | *counter.as_ref().borrow_mut() += 1; 35 | }) 36 | }) 37 | .show(ui, &mut self.cache, EXAMPLE_TEXT); 38 | }); 39 | }); 40 | } 41 | } 42 | 43 | fn main() -> eframe::Result { 44 | let mut args = std::env::args(); 45 | args.next(); 46 | 47 | eframe::run_native( 48 | "Markdown viewer", 49 | eframe::NativeOptions::default(), 50 | Box::new(move |cc| { 51 | if let Some(theme) = args.next() { 52 | if theme == "light" { 53 | cc.egui_ctx.set_theme(egui::Theme::Light); 54 | } else if theme == "dark" { 55 | cc.egui_ctx.set_theme(egui::Theme::Dark); 56 | } 57 | } 58 | 59 | cc.egui_ctx.style_mut(|style| { 60 | // Show the url of a hyperlink on hover 61 | style.url_in_tooltip = true; 62 | }); 63 | 64 | Ok(Box::new(App { 65 | cache: CommonMarkCache::default(), 66 | counter: Rc::new(RefCell::new(0)), 67 | })) 68 | }), 69 | ) 70 | } 71 | 72 | const EXAMPLE_TEXT: &str = r#" 73 | # Customized rendering using html 74 |

75 | some text 76 |

77 | 78 |

79 | some text 2 80 |

81 | "#; 82 | -------------------------------------------------------------------------------- /egui_commonmark/examples/interactive.rs: -------------------------------------------------------------------------------- 1 | //! Make sure to run this example from the repo directory and not the example 2 | //! directory. To see all the features in full effect, run this example with 3 | //! `cargo r --features better_syntax_highlighting,svg,fetch` 4 | //! Add `light` or `dark` to the end of the command to specify theme. Default 5 | //! is system theme. `cargo r --features better_syntax_highlighting,svg,fetch -- dark` 6 | //! 7 | //! An easy way to visualize rendered markdown interactively 8 | 9 | use eframe::egui; 10 | use egui_commonmark::*; 11 | 12 | struct App { 13 | cache: CommonMarkCache, 14 | markdown: String, 15 | } 16 | 17 | impl eframe::App for App { 18 | fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { 19 | egui::CentralPanel::default().show(ctx, |ui| { 20 | egui::SidePanel::left("left_panel") 21 | .show_inside(ui, |ui| ui.text_edit_multiline(&mut self.markdown)); 22 | egui::CentralPanel::default().show_inside(ui, |ui| { 23 | egui::ScrollArea::vertical().show(ui, |ui| { 24 | CommonMarkViewer::new().show(ui, &mut self.cache, &self.markdown); 25 | }); 26 | }); 27 | }); 28 | } 29 | } 30 | 31 | fn main() -> eframe::Result { 32 | let mut args = std::env::args(); 33 | args.next(); 34 | 35 | eframe::run_native( 36 | "Interactive markdown viewer", 37 | eframe::NativeOptions::default(), 38 | Box::new(move |cc| { 39 | if let Some(theme) = args.next() { 40 | if theme == "light" { 41 | cc.egui_ctx.set_theme(egui::Theme::Light); 42 | } else if theme == "dark" { 43 | cc.egui_ctx.set_theme(egui::Theme::Dark); 44 | } 45 | } 46 | 47 | cc.egui_ctx.style_mut(|style| { 48 | // Show the url of a hyperlink on hover 49 | style.url_in_tooltip = true; 50 | }); 51 | 52 | Ok(Box::new(App { 53 | markdown: r#"# Heading 54 | 55 | text with a \ 56 | break 57 | 58 | text with a large 59 | 60 | separator 61 | 62 | ```python 63 | if __name__ == "__main__": 64 | pass 65 | ```"# 66 | .to_owned(), 67 | cache: CommonMarkCache::default(), 68 | })) 69 | }), 70 | ) 71 | } 72 | -------------------------------------------------------------------------------- /egui_commonmark/examples/link_hooks.rs: -------------------------------------------------------------------------------- 1 | //! Add `light` or `dark` to the end of the command to specify theme. Default 2 | //! is system theme. `cargo r --example link_hooks -- dark` 3 | 4 | use eframe::egui; 5 | use egui_commonmark::*; 6 | 7 | struct App { 8 | cache: CommonMarkCache, 9 | curr_page: usize, 10 | } 11 | 12 | impl eframe::App for App { 13 | fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { 14 | let p1 = r#"# Page 1 15 | Check out the [next](#next) page."#; 16 | let p2 = r#"# Page 2 17 | Check out the [previous](#prev) page. 18 | 19 | Notice how the destination is not shown on [hover](#prev) unlike with [urls](https://www.example.org) 20 | "#; 21 | 22 | let p = [p1, p2]; 23 | if self.cache.get_link_hook("#next").unwrap() { 24 | self.curr_page = 1; 25 | } else if self.cache.get_link_hook("#prev").unwrap() { 26 | self.curr_page = 0; 27 | } 28 | 29 | egui::CentralPanel::default().show(ctx, |ui| { 30 | egui::ScrollArea::vertical().show(ui, |ui| { 31 | CommonMarkViewer::new().show(ui, &mut self.cache, p[self.curr_page]); 32 | }); 33 | }); 34 | } 35 | } 36 | 37 | fn main() -> eframe::Result { 38 | let mut args = std::env::args(); 39 | args.next(); 40 | 41 | eframe::run_native( 42 | "Markdown viewer link hooks", 43 | eframe::NativeOptions::default(), 44 | Box::new(move |cc| { 45 | if let Some(theme) = args.next() { 46 | if theme == "light" { 47 | cc.egui_ctx.set_theme(egui::Theme::Light); 48 | } else if theme == "dark" { 49 | cc.egui_ctx.set_theme(egui::Theme::Dark); 50 | } 51 | } 52 | 53 | cc.egui_ctx.style_mut(|style| { 54 | // Show the url of a hyperlink on hover. The demonstration of 55 | // the link hooks would be a little pointless without this 56 | style.url_in_tooltip = true; 57 | }); 58 | 59 | let mut cache = CommonMarkCache::default(); 60 | cache.add_link_hook("#next"); 61 | cache.add_link_hook("#prev"); 62 | 63 | Ok(Box::new(App { 64 | cache, 65 | curr_page: 0, 66 | })) 67 | }), 68 | ) 69 | } 70 | -------------------------------------------------------------------------------- /egui_commonmark/examples/macros.rs: -------------------------------------------------------------------------------- 1 | //! Make sure to run this example from the repo directory and not the example 2 | //! directory. To see all the features in full effect, run this example with 3 | //! `cargo r --example macro --features macros,better_syntax_highlighting` 4 | //! Add `light` or `dark` to the end of the command to specify theme. Default 5 | //! is system theme. `cargo r --example macro --features macros,better_syntax_highlighting -- dark` 6 | 7 | use eframe::egui; 8 | use egui_commonmark::*; 9 | 10 | struct App { 11 | cache: CommonMarkCache, 12 | } 13 | 14 | impl eframe::App for App { 15 | fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { 16 | egui::CentralPanel::default().show(ctx, |ui| { 17 | egui::ScrollArea::vertical().show(ui, |ui| { 18 | // Embed text directly 19 | commonmark!(ui, &mut self.cache, "Hello, world"); 20 | 21 | // In cases like these it's better to use egui::Separator directly 22 | commonmark!(ui, &mut self.cache, "------------"); 23 | 24 | // From a file like include_str! NOTE: This does not cause a recompile when the 25 | // file has changed! 26 | commonmark_str!( 27 | ui, 28 | &mut self.cache, 29 | "egui_commonmark/examples/markdown/hello_world.md" 30 | ); 31 | commonmark!(ui, &mut self.cache, "------------"); 32 | 33 | commonmark_str!( 34 | ui, 35 | &mut self.cache, 36 | "egui_commonmark/examples/markdown/headers.md" 37 | ); 38 | commonmark!(ui, &mut self.cache, "------------"); 39 | 40 | commonmark_str!( 41 | ui, 42 | &mut self.cache, 43 | "egui_commonmark/examples/markdown/lists.md" 44 | ); 45 | 46 | commonmark!(ui, &mut self.cache, "------------"); 47 | 48 | commonmark_str!( 49 | ui, 50 | &mut self.cache, 51 | "egui_commonmark/examples/markdown/code-blocks.md" 52 | ); 53 | 54 | commonmark!(ui, &mut self.cache, "------------"); 55 | 56 | commonmark_str!( 57 | ui, 58 | &mut self.cache, 59 | "egui_commonmark/examples/markdown/blockquotes.md" 60 | ); 61 | 62 | commonmark!(ui, &mut self.cache, "------------"); 63 | 64 | // The table will end up with the same id as the table in the hello_world file. 65 | // Providing the id explicitly is annoying for all other widgets that are not tables 66 | // so push_id must be used in this case. 67 | ui.push_id("tables", |ui| { 68 | commonmark_str!( 69 | ui, 70 | &mut self.cache, 71 | "egui_commonmark/examples/markdown/tables.md" 72 | ); 73 | }); 74 | 75 | commonmark!(ui, &mut self.cache, "------------"); 76 | 77 | commonmark_str!( 78 | ui, 79 | &mut self.cache, 80 | "egui_commonmark/examples/markdown/definition_list.md" 81 | ); 82 | commonmark!(ui, &mut self.cache, "------------"); 83 | }); 84 | }); 85 | } 86 | } 87 | 88 | fn main() -> eframe::Result { 89 | let mut args = std::env::args(); 90 | args.next(); 91 | 92 | eframe::run_native( 93 | "Markdown viewer", 94 | eframe::NativeOptions::default(), 95 | Box::new(move |cc| { 96 | if let Some(theme) = args.next() { 97 | if theme == "light" { 98 | cc.egui_ctx.set_theme(egui::Theme::Light); 99 | } else if theme == "dark" { 100 | cc.egui_ctx.set_theme(egui::Theme::Dark); 101 | } 102 | } 103 | 104 | cc.egui_ctx.style_mut(|style| { 105 | // Show the url of a hyperlink on hover 106 | style.url_in_tooltip = true; 107 | }); 108 | 109 | Ok(Box::new(App { 110 | cache: CommonMarkCache::default(), 111 | })) 112 | }), 113 | ) 114 | } 115 | -------------------------------------------------------------------------------- /egui_commonmark/examples/markdown/blockquotes.md: -------------------------------------------------------------------------------- 1 | # Block quotes 2 | 3 | > This is a simple block quote 4 | 5 | > A block quote with more other blocks inside it 6 | > 7 | > ```rust 8 | > fn main() { 9 | > println!("Hello, World!"); 10 | > } 11 | > ``` 12 | 13 | ## Alerts 14 | 15 | Alerts build upon block quotes. 16 | 17 | ```markdown 18 | > [!NOTE] 19 | > note alert 20 | ``` 21 | 22 | or 23 | 24 | ```markdown 25 | > [!NOTE] 26 | > 27 | > note alert 28 | ``` 29 | 30 | will be displayed as: 31 | 32 | > [!NOTE] 33 | > note alert 34 | 35 | > [!TIP] 36 | > tip alert 37 | 38 | 39 | 40 | > [!imporTant] 41 | > important alert 42 | 43 | > [!WARNING] 44 | > warning alert 45 | 46 | > [!CAUTION] 47 | > 48 | > caution alert 49 | 50 | The alerts are completely customizable. An arbitrary amount of alerts can be 51 | added 52 | -------------------------------------------------------------------------------- /egui_commonmark/examples/markdown/code-blocks.md: -------------------------------------------------------------------------------- 1 | # Code blocks 2 | 3 | ```rs 4 | use egui_commonmark::*; 5 | let markdown = 6 | r"# Hello world 7 | 8 | * A list 9 | * [ ] Checkbox 10 | "; 11 | 12 | let mut cache = CommonMarkCache::default(); 13 | CommonMarkViewer::new("viewer").show(ui, &mut cache, markdown); 14 | ``` 15 | 16 | The `better_syntax_highlighting` feature does not have toml highlighting by 17 | default. It will therefore fallback to default highlighting. 18 | 19 | ```toml 20 | egui_commonmark = "0.10" 21 | image = { version = "0.24", default-features = false, features = ["png"] } 22 | ``` 23 | 24 | - ```rs 25 | let x = 3.14; 26 | ``` 27 | - Code blocks can be in lists too :) 28 | 29 | 30 | More content... 31 | 32 | Inline code blocks are supported if you for some reason need them 33 | -------------------------------------------------------------------------------- /egui_commonmark/examples/markdown/definition_list.md: -------------------------------------------------------------------------------- 1 | # Definition list 2 | 3 | **Term 1** 4 | 5 | : Definition 1 6 | 7 | **Term 2** 8 | 9 | : Definition 2 10 | 11 | ```rs 12 | let x = 3 13 | ``` 14 | 15 | Third paragraph of definition 2. 16 | 17 | An easy way to have indentation without the bullet points of lists 18 | -------------------------------------------------------------------------------- /egui_commonmark/examples/markdown/embedded_image.md: -------------------------------------------------------------------------------- 1 | # Embedded image 2 | 3 | Requires the `embedded_image` feature to be enabled which will install a 4 | bytes loader globally. 5 | 6 | ![Embedded image](data:image/png;base64,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) 7 | -------------------------------------------------------------------------------- /egui_commonmark/examples/markdown/headers.md: -------------------------------------------------------------------------------- 1 | 2 | # Header 1 3 | 4 | ## Header 2 5 | 6 | ### Header 3 7 | 8 | #### Header 4 9 | 10 | ##### Header 5 11 | 12 | ###### Header 6 13 | 14 | -------------------------------------------------------------------------------- /egui_commonmark/examples/markdown/hello_world.md: -------------------------------------------------------------------------------- 1 | # Commonmark Viewer Example 2 | 3 | A *bunch* ~~of~~ __different__ `text` styles. 4 | 5 | 6 | ![Ferris](egui_commonmark/examples/cuddlyferris.png) 7 | 8 | | __A table!__ | 9 | | ------------ | 10 | | Aa | 11 | | *Bb* | 12 | | Cc | 13 | 14 | [Link to repo](https://github.com/lampsitter/egui_commonmark) 15 | 16 | ```rs 17 | let mut vec = Vec::new(); 18 | vec.push(5); 19 | ``` 20 | 21 | > Some smart quote here 22 | 23 | - [ ] A feature[^1] 24 | - [X] A completed feature 25 | 1. Sub item 26 | 27 | [^1]: A footnote 28 | 29 | # Header 1 30 | ## Header 2 31 | ### Header 3 32 | #### Header 4 33 | ##### Header 5 34 | ###### Header 6 35 | 36 | Some text. 37 | -------------------------------------------------------------------------------- /egui_commonmark/examples/markdown/html.md: -------------------------------------------------------------------------------- 1 | # Html 2 | 3 | Inline Html is rendered as text 4 | 5 |

6 | Same goes for 7 | html blocks 8 |

9 | 10 | Text after 11 | 12 | -------------------------------------------------------------------------------- /egui_commonmark/examples/markdown/lists.md: -------------------------------------------------------------------------------- 1 | - a 2 | 1. a2 3 | 2. a3 4 | 3. a4 5 | - b 6 | 1. b 7 | - [ ] tasklist 8 | - [X] tasklist done 9 | 2. c 10 | - c 11 | - g 12 | - g1 13 | - g2 14 | - h 15 | 16 | ------------------- 17 | 18 | 1. 19 | 1. Lorem ipsum dolor sit amet, consectetur __adipiscing elit, sed__ do 20 | eiusmod tempor incididunt _ut_ labore ~~et~~ dolore magna aliqua. Ut enim 21 | ad minim veniam, quis nostrud exercitation 22 | 2. Lorem ipsum dolor sit amet, consectetur __adipiscing elit, sed__ do 23 | eiusmod tempor incididunt _ut_ labore ~~et~~ dolore magna aliqua. Ut enim 24 | ad minim veniam, quis nostrud exercitation 25 | - Lorem ipsum dolor sit amet, consectetur __adipiscing elit, sed__ do 26 | eiusmod tempor incididunt _ut_ labore ~~et~~ dolore magna aliqua. Ut enim 27 | ad minim veniam, quis nostrud exercitation 28 | 29 | -------------------------------------------------------------------------------- /egui_commonmark/examples/markdown/tables.md: -------------------------------------------------------------------------------- 1 | # Tables 2 | 3 | Column A | Column B 4 | -----------|---------- 5 | `item` `a1` | item b1 6 | item a2 | item b2 7 | item a3 | item b3 8 | item a4 | item b4 9 | 10 | 11 | 12 | 13 | Column A | Column B 14 | -----------|---------- 15 | `item` `a1` | item b1 16 | item a2 | item b2 17 | item a3 | item b3 18 | item a4 | item b4 19 | 20 | -------------------------------------------------------------------------------- /egui_commonmark/examples/mixing.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui; 2 | 3 | macro_rules! m { 4 | ($ui:expr, $cache:expr,$($a:expr),* $(,)? ) => { 5 | $( 6 | $ui.label("Label!"); 7 | #[cfg(feature = "macros")] 8 | { 9 | egui_commonmark_macros::commonmark!($ui, &mut $cache, $a); 10 | } 11 | #[cfg(not(feature = "macros"))] 12 | { 13 | egui_commonmark::CommonMarkViewer::new().show($ui, &mut $cache, $a); 14 | } 15 | )* 16 | }; 17 | } 18 | 19 | #[cfg(feature = "macros")] 20 | const WINDOW_NAME: &str = "Mixed egui and markdown (macro version)"; 21 | #[cfg(not(feature = "macros"))] 22 | const WINDOW_NAME: &str = "Mixed egui and markdown (normal version)"; 23 | 24 | // This is more of an test... 25 | // Ensure that there are no newlines that should not be present when mixing markdown 26 | // and egui widgets. 27 | fn main() -> eframe::Result<()> { 28 | let mut cache = egui_commonmark::CommonMarkCache::default(); 29 | 30 | eframe::run_simple_native(WINDOW_NAME, Default::default(), move |ctx, _frame| { 31 | egui::CentralPanel::default().show(ctx, |ui| { 32 | egui::ScrollArea::vertical().show(ui, |ui| { 33 | m!( 34 | ui, 35 | cache, 36 | "Markdown *a*", 37 | "# Markdown (Deliberate space above)", 38 | "--------------------", 39 | r#" 40 | - Simple list 1 41 | - Simple list 2 42 | "#, 43 | r#" 44 | 1. aaa 45 | 2. aaa 46 | - abb 47 | - acc 48 | 3. bbb 49 | - baa 50 | "#, 51 | r#" 52 | ```rust 53 | let x = 3; 54 | ``` 55 | "#, 56 | r#" 57 | let x = 3; 58 | "#, 59 | r#" 60 | A footnote [^F1] 61 | 62 | [^F1]: The footnote"#, 63 | r#" 64 | > 65 | > Test 66 | > 67 | "#, 68 | r#" 69 | > [!TIP] 70 | > 71 | > Test 72 | "#, 73 | r#" 74 | 75 | Column A | Column B 76 | -----------|---------- 77 | `item` `a1` | item b1 78 | item a2 | item b2 79 | item a3 | item b3 80 | item a4 | item b4 81 | 82 | "#, 83 | r#" 84 | ![Ferris](egui_commonmark/examples/cuddlyferris.png) 85 | "#, 86 | r#" 87 | [Link to repo](https://github.com/lampsitter/egui_commonmark) 88 | "#, 89 | r#" 90 | Term 1 91 | 92 | : Definition 1 93 | 94 | Term 2 95 | 96 | : Definition 2 97 | 98 | Paragraph 2 99 | 100 | Term 3 101 | 102 | : Definition 3 103 | "#, 104 | r#" 105 | Inline html"#, 106 | r#" 107 |

108 | Html 109 | block 110 |

"#, 111 | ); 112 | 113 | ui.label("Label!"); 114 | }); 115 | }); 116 | }) 117 | } 118 | -------------------------------------------------------------------------------- /egui_commonmark/examples/scroll.rs: -------------------------------------------------------------------------------- 1 | //! Make sure to run this example from the repo directory and not the example 2 | //! directory. To see all the features in full effect, run this example with 3 | //! `cargo r --features better_syntax_highlighting,svg,fetch` 4 | //! 5 | //! Add `light` or `dark` to the end of the command to specify theme. Default 6 | //! is system theme. `cargo r --example scroll --all-features dark` 7 | 8 | use eframe::egui; 9 | use egui_commonmark::*; 10 | 11 | struct App { 12 | cache: CommonMarkCache, 13 | } 14 | 15 | impl eframe::App for App { 16 | fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { 17 | let mut text = r#"# Commonmark Viewer Example 18 | This is a fairly large markdown file showcasing scroll. 19 | "# 20 | .to_string(); 21 | 22 | let repeating = r#" 23 | This section will be repeated 24 | 25 | ```rs 26 | let mut vec = Vec::new(); 27 | vec.push(5); 28 | ``` 29 | 30 | # Plans 31 | * Make a sandwich 32 | * Bake a cake 33 | * Conquer the world 34 | "#; 35 | text += &repeating.repeat(1024); 36 | 37 | egui::CentralPanel::default().show(ctx, |ui| { 38 | CommonMarkViewer::new() 39 | .max_image_width(Some(512)) 40 | .show_scrollable("viewer", ui, &mut self.cache, &text); 41 | }); 42 | } 43 | } 44 | 45 | fn main() -> eframe::Result { 46 | let mut args = std::env::args(); 47 | args.next(); 48 | 49 | eframe::run_native( 50 | "Markdown viewer", 51 | eframe::NativeOptions::default(), 52 | Box::new(move |cc| { 53 | if let Some(theme) = args.next() { 54 | if theme == "light" { 55 | cc.egui_ctx.set_theme(egui::Theme::Light); 56 | } else if theme == "dark" { 57 | cc.egui_ctx.set_theme(egui::Theme::Dark); 58 | } 59 | } 60 | 61 | cc.egui_ctx.style_mut(|style| { 62 | // Show the url of a hyperlink on hover 63 | style.url_in_tooltip = true; 64 | }); 65 | 66 | Ok(Box::new(App { 67 | cache: CommonMarkCache::default(), 68 | })) 69 | }), 70 | ) 71 | } 72 | -------------------------------------------------------------------------------- /egui_commonmark/examples/show_mut.rs: -------------------------------------------------------------------------------- 1 | //! Add `light` or `dark` to the end of the command to specify theme. Default 2 | //! is light. `cargo r --example show_mut -- dark` 3 | 4 | use eframe::egui; 5 | use egui_commonmark::*; 6 | 7 | struct App { 8 | cache: CommonMarkCache, 9 | text_buffer: String, 10 | } 11 | 12 | impl eframe::App for App { 13 | fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { 14 | egui::CentralPanel::default().show(ctx, |ui| { 15 | egui::ScrollArea::vertical().show(ui, |ui| { 16 | ui.add( 17 | egui::TextEdit::multiline(&mut self.text_buffer) 18 | .code_editor() 19 | .desired_width(f32::INFINITY), 20 | ); 21 | CommonMarkViewer::new().max_image_width(Some(512)).show_mut( 22 | ui, 23 | &mut self.cache, 24 | &mut self.text_buffer, 25 | ); 26 | }); 27 | }); 28 | } 29 | } 30 | 31 | fn main() -> eframe::Result { 32 | let mut args = std::env::args(); 33 | args.next(); 34 | 35 | eframe::run_native( 36 | "Markdown viewer", 37 | eframe::NativeOptions::default(), 38 | Box::new(move |cc| { 39 | if let Some(theme) = args.next() { 40 | if theme == "light" { 41 | cc.egui_ctx.set_theme(egui::Theme::Light); 42 | } else if theme == "dark" { 43 | cc.egui_ctx.set_theme(egui::Theme::Dark); 44 | } 45 | } 46 | 47 | cc.egui_ctx.style_mut(|style| { 48 | // Show the url of a hyperlink on hover 49 | style.url_in_tooltip = true; 50 | }); 51 | 52 | Ok(Box::new(App { 53 | cache: CommonMarkCache::default(), 54 | text_buffer: EXAMPLE_TEXT.into(), 55 | })) 56 | }), 57 | ) 58 | } 59 | 60 | const EXAMPLE_TEXT: &str = " 61 | # Todo list 62 | - [x] Exist 63 | - [ ] Visit [`egui_commonmark` repo](https://github.com/lampsitter/egui_commonmark) 64 | - [ ] Notice how the top markdown text changes in response to clicking the checkmarks. 65 | - [ ] Make up your own list items, by using the editor on the top. 66 | "; 67 | -------------------------------------------------------------------------------- /egui_commonmark/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A commonmark viewer for egui 2 | //! 3 | //! # Example 4 | //! 5 | //! ``` 6 | //! # use egui_commonmark::*; 7 | //! # use egui::__run_test_ui; 8 | //! let markdown = 9 | //! r"# Hello world 10 | //! 11 | //! * A list 12 | //! * [ ] Checkbox 13 | //! "; 14 | //! 15 | //! # __run_test_ui(|ui| { 16 | //! let mut cache = CommonMarkCache::default(); 17 | //! CommonMarkViewer::new().show(ui, &mut cache, markdown); 18 | //! # }); 19 | //! 20 | //! ``` 21 | //! 22 | //! Remember to opt into the image formats you want to use! 23 | //! 24 | //! ```toml 25 | //! image = { version = "0.25", default-features = false, features = ["png"] } 26 | //! ``` 27 | //! # FAQ 28 | //! 29 | //! ## URL is not displayed when hovering over a link 30 | //! 31 | //! By default egui does not show urls when you hover hyperlinks. To enable it, 32 | //! you can do the following before calling any ui related functions: 33 | //! 34 | //! ``` 35 | //! # use egui::__run_test_ui; 36 | //! # __run_test_ui(|ui| { 37 | //! ui.style_mut().url_in_tooltip = true; 38 | //! # }); 39 | //! ``` 40 | //! 41 | //! 42 | //! # Compile time evaluation of markdown 43 | //! 44 | //! If you want to embed markdown directly the binary then you can enable the `macros` feature. 45 | //! This will do the parsing of the markdown at compile time and output egui widgets. 46 | //! 47 | //! ## Example 48 | //! 49 | //! ``` 50 | //! use egui_commonmark::{CommonMarkCache, commonmark}; 51 | //! # egui::__run_test_ui(|ui| { 52 | //! let mut cache = CommonMarkCache::default(); 53 | //! let _response = commonmark!(ui, &mut cache, "# ATX Heading Level 1"); 54 | //! # }); 55 | //! ``` 56 | //! 57 | //! Alternatively you can embed a file 58 | //! 59 | //! 60 | //! ## Example 61 | //! 62 | //! ```rust,ignore 63 | //! use egui_commonmark::{CommonMarkCache, commonmark_str}; 64 | //! # egui::__run_test_ui(|ui| { 65 | //! let mut cache = CommonMarkCache::default(); 66 | //! commonmark_str!(ui, &mut cache, "content.md"); 67 | //! # }); 68 | //! ``` 69 | //! 70 | //! For more information check out the documentation for 71 | //! [egui_commonmark_macros](https://docs.rs/crate/egui_commonmark_macros/latest) 72 | #![cfg_attr(feature = "document-features", doc = "# Features")] 73 | #![cfg_attr(feature = "document-features", doc = document_features::document_features!())] 74 | 75 | use egui::{self, Id}; 76 | 77 | mod parsers; 78 | 79 | pub use egui_commonmark_backend::alerts::{Alert, AlertBundle}; 80 | pub use egui_commonmark_backend::misc::CommonMarkCache; 81 | pub use egui_commonmark_backend::RenderHtmlFn; 82 | pub use egui_commonmark_backend::RenderMathFn; 83 | 84 | #[cfg(feature = "macros")] 85 | pub use egui_commonmark_macros::*; 86 | 87 | #[cfg(feature = "macros")] 88 | // Do not rely on this directly! 89 | #[doc(hidden)] 90 | pub use egui_commonmark_backend; 91 | 92 | use egui_commonmark_backend::*; 93 | 94 | #[derive(Debug, Default)] 95 | pub struct CommonMarkViewer<'f> { 96 | options: CommonMarkOptions<'f>, 97 | } 98 | 99 | impl<'f> CommonMarkViewer<'f> { 100 | pub fn new() -> Self { 101 | Self::default() 102 | } 103 | 104 | /// The amount of spaces a bullet point is indented. By default this is 4 105 | /// spaces. 106 | pub fn indentation_spaces(mut self, spaces: usize) -> Self { 107 | self.options.indentation_spaces = spaces; 108 | self 109 | } 110 | 111 | /// The maximum size images are allowed to be. They will be scaled down if 112 | /// they are larger 113 | pub fn max_image_width(mut self, width: Option) -> Self { 114 | self.options.max_image_width = width; 115 | self 116 | } 117 | 118 | /// The default width of the ui. This is only respected if this is larger than 119 | /// the [`max_image_width`](Self::max_image_width) 120 | pub fn default_width(mut self, width: Option) -> Self { 121 | self.options.default_width = width; 122 | self 123 | } 124 | 125 | /// Show alt text when hovering over images. By default this is enabled. 126 | pub fn show_alt_text_on_hover(mut self, show: bool) -> Self { 127 | self.options.show_alt_text_on_hover = show; 128 | self 129 | } 130 | 131 | /// Allows changing the default implicit `file://` uri scheme. 132 | /// This does nothing if [`explicit_image_uri_scheme`](`Self::explicit_image_uri_scheme`) is enabled 133 | /// 134 | /// # Example 135 | /// ``` 136 | /// # use egui_commonmark::CommonMarkViewer; 137 | /// CommonMarkViewer::new().default_implicit_uri_scheme("https://example.org/"); 138 | /// ``` 139 | pub fn default_implicit_uri_scheme>(mut self, scheme: S) -> Self { 140 | self.options.default_implicit_uri_scheme = scheme.into(); 141 | self 142 | } 143 | 144 | /// By default any image without a uri scheme such as `foo://` is assumed to 145 | /// be of the type `file://`. This assumption can sometimes be wrong or be done 146 | /// incorrectly, so if you want to always be explicit with the scheme then set 147 | /// this to `true` 148 | pub fn explicit_image_uri_scheme(mut self, use_explicit: bool) -> Self { 149 | self.options.use_explicit_uri_scheme = use_explicit; 150 | self 151 | } 152 | 153 | #[cfg(feature = "better_syntax_highlighting")] 154 | /// Set the syntax theme to be used inside code blocks in light mode 155 | pub fn syntax_theme_light>(mut self, theme: S) -> Self { 156 | self.options.theme_light = theme.into(); 157 | self 158 | } 159 | 160 | #[cfg(feature = "better_syntax_highlighting")] 161 | /// Set the syntax theme to be used inside code blocks in dark mode 162 | pub fn syntax_theme_dark>(mut self, theme: S) -> Self { 163 | self.options.theme_dark = theme.into(); 164 | self 165 | } 166 | 167 | /// Specify what kind of alerts are supported. This can also be used to localize alerts. 168 | /// 169 | /// By default [github flavoured markdown style alerts](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts) 170 | /// are used 171 | pub fn alerts(mut self, alerts: AlertBundle) -> Self { 172 | self.options.alerts = alerts; 173 | self 174 | } 175 | 176 | /// Allows rendering math. This has to be done manually as you might want a different 177 | /// implementation for the web and native. 178 | /// 179 | /// The example is template code for rendering a svg image. Make sure to enable the 180 | /// `egui_extras/svg` feature for the result to show up. 181 | /// 182 | /// ## Example 183 | /// 184 | /// ``` 185 | /// # use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Arc}; 186 | /// # use egui_commonmark::CommonMarkViewer; 187 | /// let mut math_images = Rc::new(RefCell::new(HashMap::new())); 188 | /// CommonMarkViewer::new() 189 | /// .render_math_fn(Some(&move |ui, math, inline| { 190 | /// let mut map = math_images.borrow_mut(); 191 | /// let svg = map 192 | /// .entry(math.to_string()) 193 | /// .or_insert_with(|| { 194 | /// if inline { 195 | /// // render as inline 196 | /// // dummy data for the example 197 | /// Arc::new([0]) 198 | /// } else { 199 | /// Arc::new([0]) 200 | /// } 201 | /// }); 202 | /// 203 | /// let uri = format!("{}.svg", egui::Id::from(math.to_string()).value()); 204 | /// ui.add( 205 | /// egui::Image::new(egui::ImageSource::Bytes { 206 | /// uri: uri.into(), 207 | /// bytes: egui::load::Bytes::Shared(svg.clone()), 208 | /// }) 209 | /// .fit_to_original_size(1.0), 210 | /// ); 211 | /// })); 212 | /// ``` 213 | pub fn render_math_fn(mut self, func: Option<&'f RenderMathFn>) -> Self { 214 | self.options.math_fn = func; 215 | self 216 | } 217 | 218 | /// Allows custom handling of html. Enabling this will disable plain text rendering 219 | /// of html blocks. Nodes are included in the provided text 220 | pub fn render_html_fn(mut self, func: Option<&'f RenderHtmlFn>) -> Self { 221 | self.options.html_fn = func; 222 | self 223 | } 224 | 225 | /// Shows rendered markdown 226 | pub fn show( 227 | self, 228 | ui: &mut egui::Ui, 229 | cache: &mut CommonMarkCache, 230 | text: &str, 231 | ) -> egui::InnerResponse<()> { 232 | egui_commonmark_backend::prepare_show(cache, ui.ctx()); 233 | 234 | let (response, _) = parsers::pulldown::CommonMarkViewerInternal::new().show( 235 | ui, 236 | cache, 237 | &self.options, 238 | text, 239 | None, 240 | ); 241 | 242 | response 243 | } 244 | 245 | /// Shows rendered markdown, and allows the rendered ui to mutate the source text. 246 | /// 247 | /// The only currently implemented mutation is allowing checkboxes to be toggled through the ui. 248 | pub fn show_mut( 249 | mut self, 250 | ui: &mut egui::Ui, 251 | cache: &mut CommonMarkCache, 252 | text: &mut String, 253 | ) -> egui::InnerResponse<()> { 254 | self.options.mutable = true; 255 | egui_commonmark_backend::prepare_show(cache, ui.ctx()); 256 | 257 | let (response, checkmark_events) = parsers::pulldown::CommonMarkViewerInternal::new().show( 258 | ui, 259 | cache, 260 | &self.options, 261 | text, 262 | None, 263 | ); 264 | 265 | // Update source text for checkmarks that were clicked 266 | for ev in checkmark_events { 267 | if ev.checked { 268 | text.replace_range(ev.span, "[x]") 269 | } else { 270 | text.replace_range(ev.span, "[ ]") 271 | } 272 | } 273 | 274 | response 275 | } 276 | 277 | /// Shows markdown inside a [`ScrollArea`]. 278 | /// This function is much more performant than just calling [`show`] inside a [`ScrollArea`], 279 | /// because it only renders elements that are visible. 280 | /// 281 | /// # Caveat 282 | /// 283 | /// This assumes that the markdown is static. If it does change, you have to clear the cache 284 | /// by using [`clear_scrollable_with_id`](CommonMarkCache::clear_scrollable_with_id) or 285 | /// [`clear_scrollable`](CommonMarkCache::clear_scrollable). If the content changes every frame, 286 | /// it's faster to call [`show`] directly. 287 | /// 288 | /// [`ScrollArea`]: egui::ScrollArea 289 | /// [`show`]: crate::CommonMarkViewer::show 290 | #[doc(hidden)] // Buggy in scenarios more complex than the example application 291 | #[cfg(feature = "pulldown_cmark")] 292 | pub fn show_scrollable( 293 | self, 294 | source_id: impl std::hash::Hash, 295 | ui: &mut egui::Ui, 296 | cache: &mut CommonMarkCache, 297 | text: &str, 298 | ) { 299 | egui_commonmark_backend::prepare_show(cache, ui.ctx()); 300 | parsers::pulldown::CommonMarkViewerInternal::new().show_scrollable( 301 | Id::new(source_id), 302 | ui, 303 | cache, 304 | &self.options, 305 | text, 306 | ); 307 | } 308 | } 309 | 310 | pub(crate) struct ListLevel { 311 | current_number: Option, 312 | } 313 | 314 | #[derive(Default)] 315 | pub(crate) struct List { 316 | items: Vec, 317 | has_list_begun: bool, 318 | } 319 | 320 | impl List { 321 | pub fn start_level_with_number(&mut self, start_number: u64) { 322 | self.items.push(ListLevel { 323 | current_number: Some(start_number), 324 | }); 325 | } 326 | 327 | pub fn start_level_without_number(&mut self) { 328 | self.items.push(ListLevel { 329 | current_number: None, 330 | }); 331 | } 332 | 333 | pub fn is_inside_a_list(&self) -> bool { 334 | !self.items.is_empty() 335 | } 336 | 337 | pub fn start_item(&mut self, ui: &mut egui::Ui, options: &CommonMarkOptions) { 338 | // To ensure that newlines are only inserted within the list and not before it 339 | if self.has_list_begun { 340 | newline(ui); 341 | } else { 342 | self.has_list_begun = true; 343 | } 344 | 345 | let len = self.items.len(); 346 | if let Some(item) = self.items.last_mut() { 347 | ui.label(" ".repeat((len - 1) * options.indentation_spaces)); 348 | 349 | if let Some(number) = &mut item.current_number { 350 | number_point(ui, &number.to_string()); 351 | *number += 1; 352 | } else if len > 1 { 353 | bullet_point_hollow(ui); 354 | } else { 355 | bullet_point(ui); 356 | } 357 | } else { 358 | unreachable!(); 359 | } 360 | 361 | ui.add_space(4.0); 362 | } 363 | 364 | pub fn end_level(&mut self, ui: &mut egui::Ui, insert_newline: bool) { 365 | self.items.pop(); 366 | 367 | if self.items.is_empty() && insert_newline { 368 | newline(ui); 369 | } 370 | } 371 | } 372 | -------------------------------------------------------------------------------- /egui_commonmark/src/parsers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod pulldown; 2 | -------------------------------------------------------------------------------- /egui_commonmark/src/parsers/pulldown.rs: -------------------------------------------------------------------------------- 1 | use std::iter::Peekable; 2 | use std::ops::Range; 3 | 4 | use crate::{CommonMarkCache, CommonMarkOptions}; 5 | 6 | use egui::{self, Id, Pos2, TextStyle, Ui}; 7 | 8 | use crate::List; 9 | use egui_commonmark_backend::elements::*; 10 | use egui_commonmark_backend::misc::*; 11 | use egui_commonmark_backend::pulldown::*; 12 | use pulldown_cmark::{CowStr, HeadingLevel}; 13 | 14 | /// Newline logic is constructed by the following: 15 | /// All elements try to insert a newline before them (if they are allowed) 16 | /// and end their own line. 17 | struct Newline { 18 | /// Whether an element should insert a newline before it 19 | should_start_newline: bool, 20 | /// Whether an element should end it's own line using a newline 21 | /// This will have to be set to false in cases such as when blocks are within 22 | /// a list. 23 | should_end_newline: bool, 24 | /// only false when the widget is the last one. 25 | should_end_newline_forced: bool, 26 | } 27 | 28 | impl Default for Newline { 29 | fn default() -> Self { 30 | Self { 31 | // Default as false as the first line should not have a newline above it 32 | should_start_newline: false, 33 | should_end_newline: true, 34 | should_end_newline_forced: true, 35 | } 36 | } 37 | } 38 | 39 | impl Newline { 40 | pub fn can_insert_end(&self) -> bool { 41 | self.should_end_newline && self.should_end_newline_forced 42 | } 43 | 44 | pub fn can_insert_start(&self) -> bool { 45 | self.should_start_newline 46 | } 47 | 48 | pub fn try_insert_start(&self, ui: &mut Ui) { 49 | if self.should_start_newline { 50 | newline(ui); 51 | } 52 | } 53 | 54 | pub fn try_insert_end(&self, ui: &mut Ui) { 55 | if self.can_insert_end() { 56 | newline(ui); 57 | } 58 | } 59 | } 60 | 61 | #[derive(Default)] 62 | struct DefinitionList { 63 | is_first_item: bool, 64 | is_def_list_def: bool, 65 | } 66 | 67 | pub struct CommonMarkViewerInternal { 68 | curr_table: usize, 69 | text_style: Style, 70 | list: List, 71 | link: Option, 72 | image: Option, 73 | line: Newline, 74 | code_block: Option, 75 | 76 | /// Only populated if the html_fn option has been set 77 | html_block: String, 78 | is_list_item: bool, 79 | def_list: DefinitionList, 80 | is_table: bool, 81 | is_blockquote: bool, 82 | checkbox_events: Vec, 83 | } 84 | 85 | pub(crate) struct CheckboxClickEvent { 86 | pub(crate) checked: bool, 87 | pub(crate) span: Range, 88 | } 89 | 90 | impl CommonMarkViewerInternal { 91 | pub fn new() -> Self { 92 | Self { 93 | curr_table: 0, 94 | text_style: Style::default(), 95 | list: List::default(), 96 | link: None, 97 | image: None, 98 | line: Newline::default(), 99 | is_list_item: false, 100 | def_list: Default::default(), 101 | code_block: None, 102 | html_block: String::new(), 103 | is_table: false, 104 | is_blockquote: false, 105 | checkbox_events: Vec::new(), 106 | } 107 | } 108 | } 109 | 110 | fn parser_options_math(is_math_enabled: bool) -> pulldown_cmark::Options { 111 | if is_math_enabled { 112 | parser_options() | pulldown_cmark::Options::ENABLE_MATH 113 | } else { 114 | parser_options() 115 | } 116 | } 117 | 118 | impl CommonMarkViewerInternal { 119 | /// Be aware that this acquires egui::Context internally. 120 | /// If split Id is provided then split points will be populated 121 | pub(crate) fn show( 122 | &mut self, 123 | ui: &mut egui::Ui, 124 | cache: &mut CommonMarkCache, 125 | options: &CommonMarkOptions, 126 | text: &str, 127 | split_points_id: Option, 128 | ) -> (egui::InnerResponse<()>, Vec) { 129 | let max_width = options.max_width(ui); 130 | let layout = egui::Layout::left_to_right(egui::Align::BOTTOM).with_main_wrap(true); 131 | 132 | let re = ui.allocate_ui_with_layout(egui::vec2(max_width, 0.0), layout, |ui| { 133 | ui.spacing_mut().item_spacing.x = 0.0; 134 | let height = ui.text_style_height(&TextStyle::Body); 135 | ui.set_row_height(height); 136 | 137 | let mut events = pulldown_cmark::Parser::new_ext( 138 | text, 139 | parser_options_math(options.math_fn.is_some()), 140 | ) 141 | .into_offset_iter() 142 | .enumerate() 143 | .peekable(); 144 | 145 | while let Some((index, (e, src_span))) = events.next() { 146 | let start_position = ui.next_widget_position(); 147 | let is_element_end = matches!(e, pulldown_cmark::Event::End(_)); 148 | let should_add_split_point = self.list.is_inside_a_list() && is_element_end; 149 | 150 | if events.peek().is_none() { 151 | self.line.should_end_newline_forced = false; 152 | } 153 | 154 | self.process_event(ui, &mut events, e, src_span, cache, options, max_width); 155 | 156 | if let Some(source_id) = split_points_id { 157 | if should_add_split_point { 158 | let scroll_cache = scroll_cache(cache, &source_id); 159 | let end_position = ui.next_widget_position(); 160 | 161 | let split_point_exists = scroll_cache 162 | .split_points 163 | .iter() 164 | .any(|(i, _, _)| *i == index); 165 | 166 | if !split_point_exists { 167 | scroll_cache 168 | .split_points 169 | .push((index, start_position, end_position)); 170 | } 171 | } 172 | } 173 | 174 | if index == 0 { 175 | self.line.should_start_newline = true; 176 | } 177 | } 178 | 179 | if let Some(source_id) = split_points_id { 180 | scroll_cache(cache, &source_id).page_size = 181 | Some(ui.next_widget_position().to_vec2()); 182 | } 183 | }); 184 | 185 | (re, std::mem::take(&mut self.checkbox_events)) 186 | } 187 | 188 | pub(crate) fn show_scrollable( 189 | &mut self, 190 | source_id: Id, 191 | ui: &mut egui::Ui, 192 | cache: &mut CommonMarkCache, 193 | options: &CommonMarkOptions, 194 | text: &str, 195 | ) { 196 | let available_size = ui.available_size(); 197 | let scroll_id = source_id.with("_scroll_area"); 198 | 199 | let Some(page_size) = scroll_cache(cache, &source_id).page_size else { 200 | egui::ScrollArea::vertical() 201 | .id_salt(scroll_id) 202 | .auto_shrink([false, true]) 203 | .show(ui, |ui| { 204 | self.show(ui, cache, options, text, Some(source_id)); 205 | }); 206 | // Prevent repopulating points twice at startup 207 | scroll_cache(cache, &source_id).available_size = available_size; 208 | return; 209 | }; 210 | 211 | let events = 212 | pulldown_cmark::Parser::new_ext(text, parser_options_math(options.math_fn.is_some())) 213 | .into_offset_iter() 214 | .collect::>(); 215 | 216 | let num_rows = events.len(); 217 | 218 | egui::ScrollArea::vertical() 219 | .id_salt(scroll_id) 220 | // Elements have different widths, so the scroll area cannot try to shrink to the 221 | // content, as that will mean that the scroll bar will move when loading elements 222 | // with different widths. 223 | .auto_shrink([false, true]) 224 | .show_viewport(ui, |ui, viewport| { 225 | ui.set_height(page_size.y); 226 | let layout = egui::Layout::left_to_right(egui::Align::BOTTOM).with_main_wrap(true); 227 | 228 | let max_width = options.max_width(ui); 229 | ui.allocate_ui_with_layout(egui::vec2(max_width, 0.0), layout, |ui| { 230 | ui.spacing_mut().item_spacing.x = 0.0; 231 | let scroll_cache = scroll_cache(cache, &source_id); 232 | 233 | // finding the first element that's not in the viewport anymore 234 | let (first_event_index, _, first_end_position) = scroll_cache 235 | .split_points 236 | .iter() 237 | .filter(|(_, _, end_position)| end_position.y < viewport.min.y) 238 | .nth_back(1) 239 | .copied() 240 | .unwrap_or((0, Pos2::ZERO, Pos2::ZERO)); 241 | 242 | // finding the last element that's just outside the viewport 243 | let last_event_index = scroll_cache 244 | .split_points 245 | .iter() 246 | .filter(|(_, start_position, _)| start_position.y > viewport.max.y) 247 | .nth(1) 248 | .map(|(index, _, _)| *index) 249 | .unwrap_or(num_rows); 250 | 251 | ui.allocate_space(first_end_position.to_vec2()); 252 | 253 | // only rendering the elements that are inside the viewport 254 | let mut events = events 255 | .into_iter() 256 | .enumerate() 257 | .skip(first_event_index) 258 | .take(last_event_index - first_event_index) 259 | .peekable(); 260 | 261 | while let Some((i, (e, src_span))) = events.next() { 262 | if events.peek().is_none() { 263 | self.line.should_end_newline_forced = false; 264 | } 265 | 266 | self.process_event(ui, &mut events, e, src_span, cache, options, max_width); 267 | 268 | if i == 0 { 269 | self.line.should_start_newline = true; 270 | } 271 | } 272 | }); 273 | }); 274 | 275 | // Forcing full re-render to repopulate split points for the new size 276 | let scroll_cache = scroll_cache(cache, &source_id); 277 | if available_size != scroll_cache.available_size { 278 | scroll_cache.available_size = available_size; 279 | scroll_cache.page_size = None; 280 | scroll_cache.split_points.clear(); 281 | } 282 | } 283 | 284 | #[allow(clippy::too_many_arguments)] 285 | fn process_event<'e>( 286 | &mut self, 287 | ui: &mut Ui, 288 | events: &mut Peekable>>, 289 | event: pulldown_cmark::Event, 290 | src_span: Range, 291 | cache: &mut CommonMarkCache, 292 | options: &CommonMarkOptions, 293 | max_width: f32, 294 | ) { 295 | self.event(ui, event, src_span, cache, options, max_width); 296 | 297 | self.def_list_def_wrapping(events, max_width, cache, options, ui); 298 | self.item_list_wrapping(events, max_width, cache, options, ui); 299 | self.table(events, cache, options, ui, max_width); 300 | self.blockquote(events, max_width, cache, options, ui); 301 | } 302 | 303 | fn def_list_def_wrapping<'e>( 304 | &mut self, 305 | events: &mut Peekable>>, 306 | max_width: f32, 307 | cache: &mut CommonMarkCache, 308 | options: &CommonMarkOptions, 309 | ui: &mut Ui, 310 | ) { 311 | if self.def_list.is_def_list_def { 312 | self.def_list.is_def_list_def = false; 313 | 314 | let item_events = delayed_events(events, |tag| { 315 | matches!(tag, pulldown_cmark::TagEnd::DefinitionListDefinition) 316 | }); 317 | 318 | let mut events_iter = item_events.into_iter().enumerate().peekable(); 319 | 320 | self.line.try_insert_start(ui); 321 | 322 | // Proccess a single event separately so that we do not insert spaces where we do not 323 | // want them 324 | self.line.should_start_newline = false; 325 | if let Some((_, (e, src_span))) = events_iter.next() { 326 | self.process_event(ui, &mut events_iter, e, src_span, cache, options, max_width); 327 | } 328 | 329 | ui.label(" ".repeat(options.indentation_spaces)); 330 | self.line.should_start_newline = true; 331 | self.line.should_end_newline = false; 332 | // Required to ensure that the content is aligned with the identation 333 | ui.horizontal_wrapped(|ui| { 334 | while let Some((_, (e, src_span))) = events_iter.next() { 335 | self.process_event( 336 | ui, 337 | &mut events_iter, 338 | e, 339 | src_span, 340 | cache, 341 | options, 342 | max_width, 343 | ); 344 | } 345 | }); 346 | self.line.should_end_newline = true; 347 | 348 | // Only end the definition items line if it is not the last element in the list 349 | if !matches!( 350 | events.peek(), 351 | Some(( 352 | _, 353 | ( 354 | pulldown_cmark::Event::End(pulldown_cmark::TagEnd::DefinitionList), 355 | _ 356 | ) 357 | )) 358 | ) { 359 | self.line.try_insert_end(ui); 360 | } 361 | } 362 | } 363 | 364 | fn item_list_wrapping<'e>( 365 | &mut self, 366 | events: &mut impl Iterator>, 367 | max_width: f32, 368 | cache: &mut CommonMarkCache, 369 | options: &CommonMarkOptions, 370 | ui: &mut Ui, 371 | ) { 372 | if self.is_list_item { 373 | self.is_list_item = false; 374 | 375 | let item_events = delayed_events_list_item(events); 376 | let mut events_iter = item_events.into_iter().enumerate().peekable(); 377 | 378 | // Required to ensure that the content of the list item is aligned with 379 | // the * or - when wrapping 380 | ui.horizontal_wrapped(|ui| { 381 | while let Some((_, (e, src_span))) = events_iter.next() { 382 | self.process_event( 383 | ui, 384 | &mut events_iter, 385 | e, 386 | src_span, 387 | cache, 388 | options, 389 | max_width, 390 | ); 391 | } 392 | }); 393 | } 394 | } 395 | 396 | fn blockquote<'e>( 397 | &mut self, 398 | events: &mut Peekable>>, 399 | max_width: f32, 400 | cache: &mut CommonMarkCache, 401 | options: &CommonMarkOptions, 402 | ui: &mut Ui, 403 | ) { 404 | if self.is_blockquote { 405 | let mut collected_events = delayed_events(events, |tag| { 406 | matches!(tag, pulldown_cmark::TagEnd::BlockQuote(_)) 407 | }); 408 | self.line.try_insert_start(ui); 409 | 410 | // Currently the blockquotes are made in such a way that they need a newline at the end 411 | // and the start so when this is the first element in the markdown the newline must be 412 | // manually enabled 413 | self.line.should_start_newline = true; 414 | if let Some(alert) = parse_alerts(&options.alerts, &mut collected_events) { 415 | egui_commonmark_backend::alert_ui(alert, ui, |ui| { 416 | for (event, src_span) in collected_events { 417 | self.event(ui, event, src_span, cache, options, max_width); 418 | } 419 | }) 420 | } else { 421 | blockquote(ui, ui.visuals().weak_text_color(), |ui| { 422 | self.text_style.quote = true; 423 | for (event, src_span) in collected_events { 424 | self.event(ui, event, src_span, cache, options, max_width); 425 | } 426 | self.text_style.quote = false; 427 | }); 428 | } 429 | 430 | if events.peek().is_none() { 431 | self.line.should_end_newline_forced = false; 432 | } 433 | 434 | self.line.try_insert_end(ui); 435 | self.is_blockquote = false; 436 | } 437 | } 438 | 439 | fn table<'e>( 440 | &mut self, 441 | events: &mut Peekable>>, 442 | cache: &mut CommonMarkCache, 443 | options: &CommonMarkOptions, 444 | ui: &mut Ui, 445 | max_width: f32, 446 | ) { 447 | if self.is_table { 448 | self.line.try_insert_start(ui); 449 | 450 | let id = ui.id().with("_table").with(self.curr_table); 451 | self.curr_table += 1; 452 | 453 | egui::Frame::group(ui.style()).show(ui, |ui| { 454 | let Table { header, rows } = parse_table(events); 455 | 456 | egui::Grid::new(id).striped(true).show(ui, |ui| { 457 | for col in header { 458 | ui.horizontal(|ui| { 459 | for (e, src_span) in col { 460 | let tmp_start = 461 | std::mem::replace(&mut self.line.should_start_newline, false); 462 | let tmp_end = 463 | std::mem::replace(&mut self.line.should_end_newline, false); 464 | self.event(ui, e, src_span, cache, options, max_width); 465 | self.line.should_start_newline = tmp_start; 466 | self.line.should_end_newline = tmp_end; 467 | } 468 | }); 469 | } 470 | 471 | ui.end_row(); 472 | 473 | for row in rows { 474 | for col in row { 475 | ui.horizontal(|ui| { 476 | for (e, src_span) in col { 477 | let tmp_start = std::mem::replace( 478 | &mut self.line.should_start_newline, 479 | false, 480 | ); 481 | let tmp_end = 482 | std::mem::replace(&mut self.line.should_end_newline, false); 483 | self.event(ui, e, src_span, cache, options, max_width); 484 | self.line.should_start_newline = tmp_start; 485 | self.line.should_end_newline = tmp_end; 486 | } 487 | }); 488 | } 489 | 490 | ui.end_row(); 491 | } 492 | }); 493 | }); 494 | 495 | self.is_table = false; 496 | if events.peek().is_none() { 497 | self.line.should_end_newline_forced = false; 498 | } 499 | 500 | self.line.try_insert_end(ui); 501 | } 502 | } 503 | 504 | fn event( 505 | &mut self, 506 | ui: &mut Ui, 507 | event: pulldown_cmark::Event, 508 | src_span: Range, 509 | cache: &mut CommonMarkCache, 510 | options: &CommonMarkOptions, 511 | max_width: f32, 512 | ) { 513 | match event { 514 | pulldown_cmark::Event::Start(tag) => self.start_tag(ui, tag, options), 515 | pulldown_cmark::Event::End(tag) => self.end_tag(ui, tag, cache, options, max_width), 516 | pulldown_cmark::Event::Text(text) => { 517 | self.event_text(text, ui); 518 | } 519 | pulldown_cmark::Event::Code(text) => { 520 | self.text_style.code = true; 521 | self.event_text(text, ui); 522 | self.text_style.code = false; 523 | } 524 | pulldown_cmark::Event::InlineHtml(text) => { 525 | self.event_text(text, ui); 526 | } 527 | 528 | pulldown_cmark::Event::Html(text) => { 529 | if options.html_fn.is_some() { 530 | self.html_block.push_str(&text); 531 | } else { 532 | self.event_text(text, ui); 533 | } 534 | } 535 | pulldown_cmark::Event::FootnoteReference(footnote) => { 536 | footnote_start(ui, &footnote); 537 | } 538 | pulldown_cmark::Event::SoftBreak => { 539 | soft_break(ui); 540 | } 541 | pulldown_cmark::Event::HardBreak => newline(ui), 542 | pulldown_cmark::Event::Rule => { 543 | self.line.try_insert_start(ui); 544 | rule(ui, self.line.can_insert_end()); 545 | } 546 | pulldown_cmark::Event::TaskListMarker(mut checkbox) => { 547 | if options.mutable { 548 | if ui 549 | .add(egui::Checkbox::without_text(&mut checkbox)) 550 | .clicked() 551 | { 552 | self.checkbox_events.push(CheckboxClickEvent { 553 | checked: checkbox, 554 | span: src_span, 555 | }); 556 | } 557 | } else { 558 | ui.add(ImmutableCheckbox::without_text(&mut checkbox)); 559 | } 560 | } 561 | pulldown_cmark::Event::InlineMath(tex) => { 562 | if let Some(math_fn) = options.math_fn { 563 | math_fn(ui, &tex, true); 564 | } 565 | } 566 | pulldown_cmark::Event::DisplayMath(tex) => { 567 | if let Some(math_fn) = options.math_fn { 568 | math_fn(ui, &tex, false); 569 | } 570 | } 571 | } 572 | } 573 | 574 | fn event_text(&mut self, text: CowStr, ui: &mut Ui) { 575 | let rich_text = self.text_style.to_richtext(ui, &text); 576 | if let Some(image) = &mut self.image { 577 | image.alt_text.push(rich_text); 578 | } else if let Some(block) = &mut self.code_block { 579 | block.content.push_str(&text); 580 | } else if let Some(link) = &mut self.link { 581 | link.text.push(rich_text); 582 | } else { 583 | ui.label(rich_text); 584 | } 585 | } 586 | 587 | fn start_tag(&mut self, ui: &mut Ui, tag: pulldown_cmark::Tag, options: &CommonMarkOptions) { 588 | match tag { 589 | pulldown_cmark::Tag::Paragraph => { 590 | self.line.try_insert_start(ui); 591 | } 592 | pulldown_cmark::Tag::Heading { level, .. } => { 593 | // Headings should always insert a newline even if it is at the start. 594 | // Whether this is okay in all scenarios is a different question. 595 | newline(ui); 596 | self.text_style.heading = Some(match level { 597 | HeadingLevel::H1 => 0, 598 | HeadingLevel::H2 => 1, 599 | HeadingLevel::H3 => 2, 600 | HeadingLevel::H4 => 3, 601 | HeadingLevel::H5 => 4, 602 | HeadingLevel::H6 => 5, 603 | }); 604 | } 605 | 606 | // deliberately not using the built in alerts from pulldown-cmark as 607 | // the markdown itself cannot be localized :( e.g: [!TIP] 608 | pulldown_cmark::Tag::BlockQuote(_) => { 609 | self.is_blockquote = true; 610 | } 611 | pulldown_cmark::Tag::CodeBlock(c) => { 612 | match c { 613 | pulldown_cmark::CodeBlockKind::Fenced(lang) => { 614 | self.code_block = Some(crate::CodeBlock { 615 | lang: Some(lang.to_string()), 616 | content: "".to_string(), 617 | }); 618 | } 619 | pulldown_cmark::CodeBlockKind::Indented => { 620 | self.code_block = Some(crate::CodeBlock { 621 | lang: None, 622 | content: "".to_string(), 623 | }); 624 | } 625 | } 626 | self.line.try_insert_start(ui); 627 | } 628 | 629 | pulldown_cmark::Tag::List(point) => { 630 | if !self.list.is_inside_a_list() && self.line.can_insert_start() { 631 | newline(ui); 632 | } 633 | 634 | if let Some(number) = point { 635 | self.list.start_level_with_number(number); 636 | } else { 637 | self.list.start_level_without_number(); 638 | } 639 | self.line.should_start_newline = false; 640 | self.line.should_end_newline = false; 641 | } 642 | 643 | pulldown_cmark::Tag::Item => { 644 | self.is_list_item = true; 645 | self.list.start_item(ui, options); 646 | } 647 | 648 | pulldown_cmark::Tag::FootnoteDefinition(note) => { 649 | self.line.try_insert_start(ui); 650 | 651 | self.line.should_start_newline = false; 652 | self.line.should_end_newline = false; 653 | footnote(ui, ¬e); 654 | } 655 | pulldown_cmark::Tag::Table(_) => { 656 | self.is_table = true; 657 | } 658 | pulldown_cmark::Tag::TableHead => {} 659 | pulldown_cmark::Tag::TableRow => {} 660 | pulldown_cmark::Tag::TableCell => {} 661 | pulldown_cmark::Tag::Emphasis => { 662 | self.text_style.emphasis = true; 663 | } 664 | pulldown_cmark::Tag::Strong => { 665 | self.text_style.strong = true; 666 | } 667 | pulldown_cmark::Tag::Strikethrough => { 668 | self.text_style.strikethrough = true; 669 | } 670 | pulldown_cmark::Tag::Link { dest_url, .. } => { 671 | self.link = Some(crate::Link { 672 | destination: dest_url.to_string(), 673 | text: Vec::new(), 674 | }); 675 | } 676 | pulldown_cmark::Tag::Image { dest_url, .. } => { 677 | self.image = Some(crate::Image::new(&dest_url, options)); 678 | } 679 | pulldown_cmark::Tag::HtmlBlock => { 680 | self.line.try_insert_start(ui); 681 | } 682 | pulldown_cmark::Tag::MetadataBlock(_) => {} 683 | 684 | pulldown_cmark::Tag::DefinitionList => { 685 | self.line.try_insert_start(ui); 686 | self.def_list.is_first_item = true; 687 | } 688 | pulldown_cmark::Tag::DefinitionListTitle => { 689 | // we disable newline as the first title should not insert a newline 690 | // as we have already done that upon the DefinitionList Tag 691 | if !self.def_list.is_first_item { 692 | self.line.try_insert_start(ui) 693 | } else { 694 | self.def_list.is_first_item = false; 695 | } 696 | } 697 | pulldown_cmark::Tag::DefinitionListDefinition => { 698 | self.def_list.is_def_list_def = true; 699 | } 700 | // Not yet supported 701 | pulldown_cmark::Tag::Superscript | pulldown_cmark::Tag::Subscript => {} 702 | } 703 | } 704 | 705 | fn end_tag( 706 | &mut self, 707 | ui: &mut Ui, 708 | tag: pulldown_cmark::TagEnd, 709 | cache: &mut CommonMarkCache, 710 | options: &CommonMarkOptions, 711 | max_width: f32, 712 | ) { 713 | match tag { 714 | pulldown_cmark::TagEnd::Paragraph => { 715 | self.line.try_insert_end(ui); 716 | } 717 | pulldown_cmark::TagEnd::Heading { .. } => { 718 | self.line.try_insert_end(ui); 719 | self.text_style.heading = None; 720 | } 721 | pulldown_cmark::TagEnd::BlockQuote(_) => {} 722 | pulldown_cmark::TagEnd::CodeBlock => { 723 | self.end_code_block(ui, cache, options, max_width); 724 | } 725 | 726 | pulldown_cmark::TagEnd::List(_) => { 727 | self.line.should_start_newline = true; 728 | self.line.should_end_newline = true; 729 | 730 | self.list.end_level(ui, self.line.can_insert_end()); 731 | 732 | if !self.list.is_inside_a_list() { 733 | // Reset all the state and make it ready for the next list that occurs 734 | self.list = List::default(); 735 | } 736 | } 737 | pulldown_cmark::TagEnd::Item => {} 738 | pulldown_cmark::TagEnd::FootnoteDefinition => { 739 | self.line.should_start_newline = true; 740 | self.line.should_end_newline = true; 741 | self.line.try_insert_end(ui); 742 | } 743 | pulldown_cmark::TagEnd::Table => {} 744 | pulldown_cmark::TagEnd::TableHead => {} 745 | pulldown_cmark::TagEnd::TableRow => {} 746 | pulldown_cmark::TagEnd::TableCell => { 747 | // Ensure space between cells 748 | ui.label(" "); 749 | } 750 | pulldown_cmark::TagEnd::Emphasis => { 751 | self.text_style.emphasis = false; 752 | } 753 | pulldown_cmark::TagEnd::Strong => { 754 | self.text_style.strong = false; 755 | } 756 | pulldown_cmark::TagEnd::Strikethrough => { 757 | self.text_style.strikethrough = false; 758 | } 759 | pulldown_cmark::TagEnd::Link => { 760 | if let Some(link) = self.link.take() { 761 | link.end(ui, cache); 762 | } 763 | } 764 | pulldown_cmark::TagEnd::Image => { 765 | if let Some(image) = self.image.take() { 766 | image.end(ui, options); 767 | } 768 | } 769 | pulldown_cmark::TagEnd::HtmlBlock => { 770 | if let Some(html_fn) = options.html_fn { 771 | html_fn(ui, &self.html_block); 772 | self.html_block.clear(); 773 | } 774 | } 775 | 776 | pulldown_cmark::TagEnd::MetadataBlock(_) => {} 777 | 778 | pulldown_cmark::TagEnd::DefinitionList => self.line.try_insert_end(ui), 779 | pulldown_cmark::TagEnd::DefinitionListTitle 780 | | pulldown_cmark::TagEnd::DefinitionListDefinition => {} 781 | pulldown_cmark::TagEnd::Superscript | pulldown_cmark::TagEnd::Subscript => {} 782 | } 783 | } 784 | 785 | fn end_code_block( 786 | &mut self, 787 | ui: &mut Ui, 788 | cache: &mut CommonMarkCache, 789 | options: &CommonMarkOptions, 790 | max_width: f32, 791 | ) { 792 | if let Some(block) = self.code_block.take() { 793 | block.end(ui, cache, options, max_width); 794 | self.line.try_insert_end(ui); 795 | } 796 | } 797 | } 798 | -------------------------------------------------------------------------------- /egui_commonmark_backend/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "egui_commonmark_backend" 3 | authors = ["Erlend Walstad"] 4 | 5 | version.workspace = true 6 | edition.workspace = true 7 | license.workspace = true 8 | rust-version.workspace = true 9 | repository.workspace = true 10 | 11 | description = "Shared code for egui_commonmark and egui_commonmark_macros" 12 | keywords = ["commonmark", "egui"] 13 | categories = ["gui"] 14 | readme = "README.md" 15 | documentation = "https://docs.rs/egui_commonmark_backend" 16 | include = ["**/*.rs", "LICENSE-MIT", "LICENSE-APACHE", "Cargo.toml"] 17 | 18 | [dependencies] 19 | pulldown-cmark = { workspace = true } 20 | egui_extras = { workspace = true } 21 | egui = { workspace = true } 22 | 23 | data-url = { version = "0.3.1", optional = true } 24 | syntect = { version = "5.0.0", optional = true, default-features = false, features = [ 25 | "default-fancy", 26 | ] } 27 | 28 | [features] 29 | better_syntax_highlighting = ["dep:syntect"] 30 | embedded_image = ["dep:data-url"] 31 | -------------------------------------------------------------------------------- /egui_commonmark_backend/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /egui_commonmark_backend/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /egui_commonmark_backend/README.md: -------------------------------------------------------------------------------- 1 | # A commonmark viewer for [egui](https://github.com/emilk/egui) 2 | 3 | [![Crate](https://img.shields.io/crates/v/egui_commonmark_backend.svg)](https://crates.io/crates/egui_commonmark_backend) 4 | 5 | This contains shared code between the crates `egui_commonmark` and `egui_commonmark_macros`. 6 | See [egui_commonmark](https://github.com/lampsitter/egui_commonmark) as a starting point. 7 | 8 | ## License 9 | 10 | Licensed under either of 11 | 12 | * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 13 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 14 | 15 | at your option. 16 | -------------------------------------------------------------------------------- /egui_commonmark_backend/src/alerts.rs: -------------------------------------------------------------------------------- 1 | use crate::elements::{blockquote, newline}; 2 | use egui::Ui; 3 | use std::collections::HashMap; 4 | 5 | #[derive(Debug, Clone)] 6 | pub struct Alert { 7 | /// The color that will be used to put emphasis to the alert 8 | pub accent_color: egui::Color32, 9 | /// The icon that will be displayed 10 | pub icon: char, 11 | /// The identifier that will be used to look for the blockquote such as NOTE and TIP 12 | pub identifier: String, 13 | /// The identifier that will be shown when rendering. E.g: Note and Tip 14 | pub identifier_rendered: String, 15 | } 16 | 17 | // Seperate function to not leak into the public API 18 | pub fn alert_ui(alert: &Alert, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui)) { 19 | blockquote(ui, alert.accent_color, |ui| { 20 | newline(ui); 21 | ui.colored_label(alert.accent_color, alert.icon.to_string()); 22 | ui.add_space(3.0); 23 | ui.colored_label(alert.accent_color, &alert.identifier_rendered); 24 | // end line 25 | newline(ui); 26 | add_contents(ui); 27 | }) 28 | } 29 | 30 | #[derive(Debug, Clone)] 31 | pub struct AlertBundle { 32 | /// the key is `[!identifier]` 33 | alerts: HashMap, 34 | } 35 | 36 | impl AlertBundle { 37 | pub fn from_alerts(alerts: Vec) -> Self { 38 | let mut map = HashMap::with_capacity(alerts.len()); 39 | for alert in alerts { 40 | // Store it the way it will be in text to make lookup easier 41 | map.insert(format!("[!{}]", alert.identifier), alert); 42 | } 43 | 44 | Self { alerts: map } 45 | } 46 | 47 | pub fn into_alerts(self) -> Vec { 48 | // since the rendered field can be changed it is better to force creation of 49 | // a new bundle with from_alerts after a potential modification 50 | 51 | self.alerts.into_values().collect::>() 52 | } 53 | 54 | pub fn empty() -> Self { 55 | AlertBundle { 56 | alerts: Default::default(), 57 | } 58 | } 59 | 60 | /// github flavoured markdown alerts 61 | /// `[!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]` and `[!CAUTION]`. 62 | /// 63 | /// This is used by default 64 | pub fn gfm() -> Self { 65 | Self::from_alerts(vec![ 66 | Alert { 67 | accent_color: egui::Color32::from_rgb(10, 80, 210), 68 | icon: '❕', 69 | identifier: "NOTE".to_owned(), 70 | identifier_rendered: "Note".to_owned(), 71 | }, 72 | Alert { 73 | accent_color: egui::Color32::from_rgb(0, 130, 20), 74 | icon: '💡', 75 | identifier: "TIP".to_owned(), 76 | identifier_rendered: "Tip".to_owned(), 77 | }, 78 | Alert { 79 | accent_color: egui::Color32::from_rgb(150, 30, 140), 80 | icon: '💬', 81 | identifier: "IMPORTANT".to_owned(), 82 | identifier_rendered: "Important".to_owned(), 83 | }, 84 | Alert { 85 | accent_color: egui::Color32::from_rgb(200, 120, 0), 86 | icon: '⚠', 87 | identifier: "WARNING".to_owned(), 88 | identifier_rendered: "Warning".to_owned(), 89 | }, 90 | Alert { 91 | accent_color: egui::Color32::from_rgb(220, 0, 0), 92 | icon: '🔴', 93 | identifier: "CAUTION".to_owned(), 94 | identifier_rendered: "Caution".to_owned(), 95 | }, 96 | ]) 97 | } 98 | 99 | /// See if the bundle contains no alerts 100 | pub fn is_empty(&self) -> bool { 101 | self.alerts.is_empty() 102 | } 103 | } 104 | 105 | pub fn try_get_alert<'a>(bundle: &'a AlertBundle, text: &str) -> Option<&'a Alert> { 106 | bundle.alerts.get(&text.to_uppercase()) 107 | } 108 | -------------------------------------------------------------------------------- /egui_commonmark_backend/src/data_url_loader.rs: -------------------------------------------------------------------------------- 1 | use egui::load::{Bytes, BytesLoadResult, BytesLoader, BytesPoll, LoadError}; 2 | use egui::mutex::Mutex; 3 | 4 | use std::collections::HashMap; 5 | use std::sync::Arc; 6 | use std::task::Poll; 7 | 8 | pub fn install_loader(ctx: &egui::Context) { 9 | if !ctx.is_loader_installed(DataUrlLoader::ID) { 10 | ctx.add_bytes_loader(std::sync::Arc::new(DataUrlLoader::default())); 11 | } 12 | } 13 | 14 | #[derive(Clone)] 15 | struct Data { 16 | bytes: Arc<[u8]>, 17 | mime: Option, 18 | } 19 | 20 | type Entry = Poll>; 21 | 22 | #[derive(Default)] 23 | pub struct DataUrlLoader { 24 | cache: Arc>>, 25 | } 26 | 27 | impl DataUrlLoader { 28 | pub const ID: &'static str = egui::generate_loader_id!(DataUrlLoader); 29 | } 30 | 31 | impl BytesLoader for DataUrlLoader { 32 | fn id(&self) -> &str { 33 | Self::ID 34 | } 35 | 36 | fn load(&self, ctx: &egui::Context, uri: &str) -> BytesLoadResult { 37 | if data_url::DataUrl::process(uri).is_err() { 38 | return Err(LoadError::NotSupported); 39 | }; 40 | 41 | let mut cache = self.cache.lock(); 42 | if let Some(entry) = cache.get(uri).cloned() { 43 | match entry { 44 | Poll::Ready(Ok(file)) => Ok(BytesPoll::Ready { 45 | size: None, 46 | bytes: Bytes::Shared(file.bytes), 47 | mime: file.mime, 48 | }), 49 | Poll::Ready(Err(err)) => Err(LoadError::Loading(err)), 50 | Poll::Pending => Ok(BytesPoll::Pending { size: None }), 51 | } 52 | } else { 53 | cache.insert(uri.to_owned(), Poll::Pending); 54 | drop(cache); 55 | 56 | let cache = self.cache.clone(); 57 | let uri = uri.to_owned(); 58 | let ctx = ctx.clone(); 59 | 60 | std::thread::Builder::new() 61 | .name("DataUrlLoader".to_owned()) 62 | .spawn(move || { 63 | // Must unfortuntely do the process step again 64 | let url = data_url::DataUrl::process(&uri); 65 | match url { 66 | Ok(url) => { 67 | let result = url 68 | .decode_to_vec() 69 | .map(|(decoded, _)| { 70 | let mime = url.mime_type().to_string(); 71 | let mime = if mime.is_empty() { None } else { Some(mime) }; 72 | 73 | Data { 74 | bytes: decoded.into(), 75 | mime, 76 | } 77 | }) 78 | .map_err(|e| e.to_string()); 79 | cache.lock().insert(uri, Poll::Ready(result)); 80 | } 81 | Err(e) => { 82 | cache.lock().insert(uri, Poll::Ready(Err(e.to_string()))); 83 | } 84 | } 85 | 86 | ctx.request_repaint(); 87 | }) 88 | .expect("could not spawn thread"); 89 | 90 | Ok(BytesPoll::Pending { size: None }) 91 | } 92 | } 93 | 94 | fn forget(&self, uri: &str) { 95 | let _ = self.cache.lock().remove(uri); 96 | } 97 | 98 | fn forget_all(&self) { 99 | self.cache.lock().clear(); 100 | } 101 | 102 | fn byte_size(&self) -> usize { 103 | self.cache 104 | .lock() 105 | .values() 106 | .map(|entry| match entry { 107 | Poll::Ready(Ok(file)) => { 108 | file.bytes.len() + file.mime.as_ref().map_or(0, |m| m.len()) 109 | } 110 | Poll::Ready(Err(err)) => err.len(), 111 | _ => 0, 112 | }) 113 | .sum() 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /egui_commonmark_backend/src/elements.rs: -------------------------------------------------------------------------------- 1 | use egui::{self, epaint, NumExt, RichText, Sense, TextStyle, Ui, Vec2}; 2 | 3 | #[inline] 4 | pub fn rule(ui: &mut Ui, end_line: bool) { 5 | ui.add(egui::Separator::default().horizontal()); 6 | // This does not add a new line, but instead ends the separator 7 | if end_line { 8 | newline(ui); 9 | } 10 | } 11 | 12 | #[inline] 13 | pub fn soft_break(ui: &mut Ui) { 14 | ui.label(" "); 15 | } 16 | 17 | #[inline] 18 | pub fn newline(ui: &mut Ui) { 19 | ui.label("\n"); 20 | } 21 | 22 | pub fn bullet_point(ui: &mut Ui) { 23 | let (rect, _) = ui.allocate_exact_size( 24 | egui::vec2(width_body_space(ui) * 4.0, height_body(ui)), 25 | Sense::hover(), 26 | ); 27 | ui.painter().circle_filled( 28 | rect.center(), 29 | rect.height() / 6.0, 30 | ui.visuals().strong_text_color(), 31 | ); 32 | } 33 | 34 | pub fn bullet_point_hollow(ui: &mut Ui) { 35 | let (rect, _) = ui.allocate_exact_size( 36 | egui::vec2(width_body_space(ui) * 4.0, height_body(ui)), 37 | Sense::hover(), 38 | ); 39 | ui.painter().circle( 40 | rect.center(), 41 | rect.height() / 6.0, 42 | egui::Color32::TRANSPARENT, 43 | egui::Stroke::new(0.6, ui.visuals().strong_text_color()), 44 | ); 45 | } 46 | 47 | pub fn number_point(ui: &mut Ui, number: &str) { 48 | let (rect, _) = ui.allocate_exact_size( 49 | egui::vec2(width_body_space(ui) * 4.0, height_body(ui)), 50 | Sense::hover(), 51 | ); 52 | ui.painter().text( 53 | rect.right_center(), 54 | egui::Align2::RIGHT_CENTER, 55 | format!("{number}."), 56 | TextStyle::Body.resolve(ui.style()), 57 | ui.visuals().strong_text_color(), 58 | ); 59 | } 60 | 61 | #[inline] 62 | pub fn footnote_start(ui: &mut Ui, note: &str) { 63 | ui.label(RichText::new(note).raised().strong().small()); 64 | } 65 | 66 | pub fn footnote(ui: &mut Ui, text: &str) { 67 | let (rect, _) = ui.allocate_exact_size( 68 | egui::vec2(width_body_space(ui) * 4.0, height_body(ui)), 69 | Sense::hover(), 70 | ); 71 | ui.painter().text( 72 | rect.right_top(), 73 | egui::Align2::RIGHT_TOP, 74 | format!("{text}."), 75 | TextStyle::Small.resolve(ui.style()), 76 | ui.visuals().strong_text_color(), 77 | ); 78 | } 79 | 80 | fn height_body(ui: &Ui) -> f32 { 81 | ui.text_style_height(&TextStyle::Body) 82 | } 83 | 84 | fn width_body_space(ui: &Ui) -> f32 { 85 | let id = TextStyle::Body.resolve(ui.style()); 86 | ui.fonts(|f| f.glyph_width(&id, ' ')) 87 | } 88 | 89 | /// Enhanced/specialized version of egui's code blocks. This one features copy button and borders 90 | pub fn code_block<'t>( 91 | ui: &mut Ui, 92 | max_width: f32, 93 | text: &str, 94 | layouter: &'t mut dyn FnMut(&Ui, &str, f32) -> std::sync::Arc, 95 | ) { 96 | let mut text = text.strip_suffix('\n').unwrap_or(text); 97 | 98 | // To manually add background color to the code block, we imitate what 99 | // TextEdit does internally 100 | let where_to_put_background = ui.painter().add(egui::Shape::Noop); 101 | 102 | // We use a `TextEdit` to make the text selectable. 103 | // Note that we take a `&mut` to a non-`mut` `&str`, which is 104 | // the how to tell `egui` that the text is not editable. 105 | let output = egui::TextEdit::multiline(&mut text) 106 | .layouter(layouter) 107 | .desired_width(max_width) 108 | // prevent trailing lines 109 | .desired_rows(1) 110 | .show(ui); 111 | 112 | // Background color + frame (This is lost when TextEdit it not editable) 113 | let frame_rect = output.response.rect; 114 | ui.painter().set( 115 | where_to_put_background, 116 | epaint::RectShape::new( 117 | frame_rect, 118 | ui.style().noninteractive().corner_radius, 119 | ui.visuals().extreme_bg_color, 120 | ui.visuals().widgets.noninteractive.bg_stroke, 121 | egui::StrokeKind::Outside, 122 | ), 123 | ); 124 | 125 | // Copy icon 126 | let spacing = &ui.style().spacing; 127 | let position = egui::pos2( 128 | frame_rect.right_top().x - spacing.icon_width * 0.5 - spacing.button_padding.x, 129 | frame_rect.right_top().y + spacing.button_padding.y * 2.0, 130 | ); 131 | 132 | // Check if we should show ✔ instead of 🗐 if the text was copied and the mouse is hovered 133 | let persistent_id = ui.make_persistent_id(output.response.id); 134 | let copied_icon = ui.memory_mut(|m| *m.data.get_temp_mut_or_default::(persistent_id)); 135 | 136 | let copy_button = ui 137 | .put( 138 | egui::Rect { 139 | min: position, 140 | max: position, 141 | }, 142 | egui::Button::new(if copied_icon { "✔" } else { "🗐" }) 143 | .small() 144 | .frame(false) 145 | .fill(egui::Color32::TRANSPARENT), 146 | ) 147 | // workaround for a regression after egui 0.27 where the edit cursor was shown even when 148 | // hovering over the button. We try interact_cursor first to allow the cursor to be 149 | // overriden 150 | .on_hover_cursor( 151 | ui.visuals() 152 | .interact_cursor 153 | .unwrap_or(egui::CursorIcon::Default), 154 | ); 155 | 156 | // Update icon state in persistent memory 157 | if copied_icon && !copy_button.hovered() { 158 | ui.memory_mut(|m| *m.data.get_temp_mut_or_default(persistent_id) = false); 159 | } 160 | if !copied_icon && copy_button.clicked() { 161 | ui.memory_mut(|m| *m.data.get_temp_mut_or_default(persistent_id) = true); 162 | } 163 | 164 | if copy_button.clicked() { 165 | use egui::TextBuffer as _; 166 | let copy_text = if let Some(cursor) = output.cursor_range { 167 | let selected_chars = cursor.as_sorted_char_range(); 168 | let selected_text = text.char_range(selected_chars); 169 | if selected_text.is_empty() { 170 | text.to_owned() 171 | } else { 172 | selected_text.to_owned() 173 | } 174 | } else { 175 | text.to_owned() 176 | }; 177 | ui.ctx().copy_text(copy_text); 178 | } 179 | } 180 | 181 | // Stripped down version of egui's Checkbox. The only difference is that this 182 | // creates a noninteractive checkbox. ui.add_enabled could have been used instead, 183 | // but it makes the checkbox too grey. 184 | pub struct ImmutableCheckbox<'a> { 185 | checked: &'a mut bool, 186 | } 187 | 188 | impl<'a> ImmutableCheckbox<'a> { 189 | pub fn without_text(checked: &'a mut bool) -> Self { 190 | ImmutableCheckbox { checked } 191 | } 192 | } 193 | 194 | impl<'a> egui::Widget for ImmutableCheckbox<'a> { 195 | fn ui(self, ui: &mut Ui) -> egui::Response { 196 | let ImmutableCheckbox { checked } = self; 197 | 198 | let spacing = &ui.spacing(); 199 | let icon_width = spacing.icon_width; 200 | 201 | let mut desired_size = egui::vec2(icon_width, 0.0); 202 | desired_size = desired_size.at_least(Vec2::splat(spacing.interact_size.y)); 203 | desired_size.y = desired_size.y.max(icon_width); 204 | let (rect, response) = ui.allocate_exact_size(desired_size, Sense::click()); 205 | 206 | if ui.is_rect_visible(rect) { 207 | let visuals = ui.style().visuals.noninteractive(); 208 | let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect); 209 | ui.painter().add(epaint::RectShape::new( 210 | big_icon_rect.expand(visuals.expansion), 211 | visuals.corner_radius, 212 | visuals.bg_fill, 213 | visuals.bg_stroke, 214 | egui::StrokeKind::Inside, 215 | )); 216 | 217 | if *checked { 218 | // Check mark: 219 | ui.painter().add(egui::Shape::line( 220 | vec![ 221 | egui::pos2(small_icon_rect.left(), small_icon_rect.center().y), 222 | egui::pos2(small_icon_rect.center().x, small_icon_rect.bottom()), 223 | egui::pos2(small_icon_rect.right(), small_icon_rect.top()), 224 | ], 225 | visuals.fg_stroke, 226 | )); 227 | } 228 | } 229 | 230 | response 231 | } 232 | } 233 | 234 | pub fn blockquote(ui: &mut Ui, accent: egui::Color32, add_contents: impl FnOnce(&mut Ui)) { 235 | let start = ui.painter().add(egui::Shape::Noop); 236 | let response = egui::Frame::new() 237 | // offset the frame so that we can use the space for the horizontal line and other stuff 238 | // By not using a separator we have better control 239 | .outer_margin(egui::Margin { 240 | left: 10, 241 | ..Default::default() 242 | }) 243 | .show(ui, add_contents) 244 | .response; 245 | 246 | // FIXME: Add some rounding 247 | 248 | ui.painter().set( 249 | start, 250 | egui::epaint::Shape::line_segment( 251 | [ 252 | egui::pos2(response.rect.left_top().x, response.rect.left_top().y + 5.0), 253 | egui::pos2( 254 | response.rect.left_bottom().x, 255 | response.rect.left_bottom().y - 5.0, 256 | ), 257 | ], 258 | egui::Stroke::new(3.0, accent), 259 | ), 260 | ); 261 | } 262 | -------------------------------------------------------------------------------- /egui_commonmark_backend/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Shared code for egui_commonmark and egui_commonmark_macro. Breaking changes will happen and 2 | //! should ideally not be relied upon. Only items that can been seen in this documentation 3 | //! can be safely used directly. 4 | 5 | #[doc(hidden)] 6 | pub mod alerts; 7 | #[doc(hidden)] 8 | pub mod elements; 9 | #[doc(hidden)] 10 | pub mod misc; 11 | #[doc(hidden)] 12 | pub mod pulldown; 13 | 14 | #[cfg(feature = "embedded_image")] 15 | mod data_url_loader; 16 | 17 | // For ease of use in proc macros 18 | #[doc(hidden)] 19 | pub use { 20 | alerts::{alert_ui, Alert, AlertBundle}, 21 | // Pretty much every single element in this module is used by the proc macros 22 | elements::*, 23 | misc::{prepare_show, CodeBlock, CommonMarkOptions, Image, Link}, 24 | }; 25 | 26 | // The only struct that is allowed to use directly. (If one does not need egui_commonmark) 27 | pub use misc::CommonMarkCache; 28 | 29 | /// Takes [`egui::Ui`], the math text to be rendered and whether it is inline 30 | pub type RenderMathFn = dyn Fn(&mut egui::Ui, &str, bool); 31 | /// Takes [`egui::Ui`] and the html text to be rendered/used 32 | pub type RenderHtmlFn = dyn Fn(&mut egui::Ui, &str); 33 | -------------------------------------------------------------------------------- /egui_commonmark_backend/src/misc.rs: -------------------------------------------------------------------------------- 1 | use crate::alerts::AlertBundle; 2 | use egui::{text::LayoutJob, RichText, TextStyle, Ui}; 3 | use std::collections::HashMap; 4 | 5 | use crate::pulldown::ScrollableCache; 6 | 7 | #[cfg(feature = "better_syntax_highlighting")] 8 | use syntect::{ 9 | easy::HighlightLines, 10 | highlighting::{Theme, ThemeSet}, 11 | parsing::{SyntaxDefinition, SyntaxSet}, 12 | util::LinesWithEndings, 13 | }; 14 | 15 | #[cfg(feature = "better_syntax_highlighting")] 16 | const DEFAULT_THEME_LIGHT: &str = "base16-ocean.light"; 17 | #[cfg(feature = "better_syntax_highlighting")] 18 | const DEFAULT_THEME_DARK: &str = "base16-ocean.dark"; 19 | 20 | pub struct CommonMarkOptions<'f> { 21 | pub indentation_spaces: usize, 22 | pub max_image_width: Option, 23 | pub show_alt_text_on_hover: bool, 24 | pub default_width: Option, 25 | #[cfg(feature = "better_syntax_highlighting")] 26 | pub theme_light: String, 27 | #[cfg(feature = "better_syntax_highlighting")] 28 | pub theme_dark: String, 29 | pub use_explicit_uri_scheme: bool, 30 | pub default_implicit_uri_scheme: String, 31 | pub alerts: AlertBundle, 32 | /// Whether to present a mutable ui for things like checkboxes 33 | pub mutable: bool, 34 | pub math_fn: Option<&'f crate::RenderMathFn>, 35 | pub html_fn: Option<&'f crate::RenderHtmlFn>, 36 | } 37 | 38 | impl<'f> std::fmt::Debug for CommonMarkOptions<'f> { 39 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 40 | let mut s = f.debug_struct("CommonMarkOptions"); 41 | 42 | s.field("indentation_spaces", &self.indentation_spaces) 43 | .field("max_image_width", &self.max_image_width) 44 | .field("show_alt_text_on_hover", &self.show_alt_text_on_hover) 45 | .field("default_width", &self.default_width); 46 | 47 | #[cfg(feature = "better_syntax_highlighting")] 48 | s.field("theme_light", &self.theme_light) 49 | .field("theme_dark", &self.theme_dark); 50 | 51 | s.field("use_explicit_uri_scheme", &self.use_explicit_uri_scheme) 52 | .field( 53 | "default_implicit_uri_scheme", 54 | &self.default_implicit_uri_scheme, 55 | ) 56 | .field("alerts", &self.alerts) 57 | .field("mutable", &self.mutable) 58 | .finish() 59 | } 60 | } 61 | 62 | impl Default for CommonMarkOptions<'_> { 63 | fn default() -> Self { 64 | Self { 65 | indentation_spaces: 4, 66 | max_image_width: None, 67 | show_alt_text_on_hover: true, 68 | default_width: None, 69 | #[cfg(feature = "better_syntax_highlighting")] 70 | theme_light: DEFAULT_THEME_LIGHT.to_owned(), 71 | #[cfg(feature = "better_syntax_highlighting")] 72 | theme_dark: DEFAULT_THEME_DARK.to_owned(), 73 | use_explicit_uri_scheme: false, 74 | default_implicit_uri_scheme: "file://".to_owned(), 75 | alerts: AlertBundle::gfm(), 76 | mutable: false, 77 | math_fn: None, 78 | html_fn: None, 79 | } 80 | } 81 | } 82 | 83 | impl CommonMarkOptions<'_> { 84 | #[cfg(feature = "better_syntax_highlighting")] 85 | pub fn curr_theme(&self, ui: &Ui) -> &str { 86 | if ui.style().visuals.dark_mode { 87 | &self.theme_dark 88 | } else { 89 | &self.theme_light 90 | } 91 | } 92 | 93 | pub fn max_width(&self, ui: &Ui) -> f32 { 94 | let max_image_width = self.max_image_width.unwrap_or(0) as f32; 95 | let available_width = ui.available_width(); 96 | 97 | let max_width = max_image_width.max(available_width); 98 | if let Some(default_width) = self.default_width { 99 | if default_width as f32 > max_width { 100 | default_width as f32 101 | } else { 102 | max_width 103 | } 104 | } else { 105 | max_width 106 | } 107 | } 108 | } 109 | 110 | #[derive(Default, Clone)] 111 | pub struct Style { 112 | pub heading: Option, 113 | pub strong: bool, 114 | pub emphasis: bool, 115 | pub strikethrough: bool, 116 | pub quote: bool, 117 | pub code: bool, 118 | } 119 | 120 | impl Style { 121 | pub fn to_richtext(&self, ui: &Ui, text: &str) -> RichText { 122 | let mut text = RichText::new(text); 123 | 124 | if let Some(level) = self.heading { 125 | let max_height = ui 126 | .style() 127 | .text_styles 128 | .get(&TextStyle::Heading) 129 | .map_or(32.0, |d| d.size); 130 | let min_height = ui 131 | .style() 132 | .text_styles 133 | .get(&TextStyle::Body) 134 | .map_or(14.0, |d| d.size); 135 | let diff = max_height - min_height; 136 | 137 | match level { 138 | 0 => { 139 | text = text.strong().heading(); 140 | } 141 | 1 => { 142 | let size = min_height + diff * 0.835; 143 | text = text.strong().size(size); 144 | } 145 | 2 => { 146 | let size = min_height + diff * 0.668; 147 | text = text.strong().size(size); 148 | } 149 | 3 => { 150 | let size = min_height + diff * 0.501; 151 | text = text.strong().size(size); 152 | } 153 | 4 => { 154 | let size = min_height + diff * 0.334; 155 | text = text.size(size); 156 | } 157 | // We only support 6 levels 158 | 5.. => { 159 | let size = min_height + diff * 0.167; 160 | text = text.size(size); 161 | } 162 | } 163 | } 164 | 165 | if self.quote { 166 | text = text.weak(); 167 | } 168 | 169 | if self.strong { 170 | text = text.strong(); 171 | } 172 | 173 | if self.emphasis { 174 | // FIXME: Might want to add some space between the next text 175 | text = text.italics(); 176 | } 177 | 178 | if self.strikethrough { 179 | text = text.strikethrough(); 180 | } 181 | 182 | if self.code { 183 | text = text.code(); 184 | } 185 | 186 | text 187 | } 188 | } 189 | 190 | #[derive(Default)] 191 | pub struct Link { 192 | pub destination: String, 193 | pub text: Vec, 194 | } 195 | 196 | impl Link { 197 | pub fn end(self, ui: &mut Ui, cache: &mut CommonMarkCache) { 198 | let Self { destination, text } = self; 199 | 200 | let mut layout_job = LayoutJob::default(); 201 | for t in text { 202 | t.append_to( 203 | &mut layout_job, 204 | ui.style(), 205 | egui::FontSelection::Default, 206 | egui::Align::LEFT, 207 | ); 208 | } 209 | if cache.link_hooks().contains_key(&destination) { 210 | let ui_link = ui.link(layout_job); 211 | if ui_link.clicked() || ui_link.middle_clicked() { 212 | cache.link_hooks_mut().insert(destination, true); 213 | } 214 | } else { 215 | ui.hyperlink_to(layout_job, destination); 216 | } 217 | } 218 | } 219 | 220 | pub struct Image { 221 | pub uri: String, 222 | pub alt_text: Vec, 223 | } 224 | 225 | impl Image { 226 | // FIXME: string conversion 227 | pub fn new(uri: &str, options: &CommonMarkOptions) -> Self { 228 | let has_scheme = uri.contains("://") || uri.starts_with("data:"); 229 | let uri = if options.use_explicit_uri_scheme || has_scheme { 230 | uri.to_string() 231 | } else { 232 | // Assume file scheme 233 | format!("{}{uri}", options.default_implicit_uri_scheme) 234 | }; 235 | 236 | Self { 237 | uri, 238 | alt_text: Vec::new(), 239 | } 240 | } 241 | 242 | pub fn end(self, ui: &mut Ui, options: &CommonMarkOptions) { 243 | let response = ui.add( 244 | egui::Image::from_uri(&self.uri) 245 | .fit_to_original_size(1.0) 246 | .max_width(options.max_width(ui)), 247 | ); 248 | 249 | if !self.alt_text.is_empty() && options.show_alt_text_on_hover { 250 | response.on_hover_ui_at_pointer(|ui| { 251 | for alt in self.alt_text { 252 | ui.label(alt); 253 | } 254 | }); 255 | } 256 | } 257 | } 258 | 259 | pub struct CodeBlock { 260 | pub lang: Option, 261 | pub content: String, 262 | } 263 | 264 | impl CodeBlock { 265 | pub fn end( 266 | &self, 267 | ui: &mut Ui, 268 | cache: &mut CommonMarkCache, 269 | options: &CommonMarkOptions, 270 | max_width: f32, 271 | ) { 272 | ui.scope(|ui| { 273 | Self::pre_syntax_highlighting(cache, options, ui); 274 | 275 | let mut layout = |ui: &Ui, string: &str, wrap_width: f32| { 276 | let mut job = if let Some(lang) = &self.lang { 277 | self.syntax_highlighting(cache, options, lang, ui, string) 278 | } else { 279 | plain_highlighting(ui, string) 280 | }; 281 | 282 | job.wrap.max_width = wrap_width; 283 | ui.fonts(|f| f.layout_job(job)) 284 | }; 285 | 286 | crate::elements::code_block(ui, max_width, &self.content, &mut layout); 287 | }); 288 | } 289 | } 290 | 291 | #[cfg(not(feature = "better_syntax_highlighting"))] 292 | impl CodeBlock { 293 | fn pre_syntax_highlighting( 294 | _cache: &mut CommonMarkCache, 295 | _options: &CommonMarkOptions, 296 | ui: &mut Ui, 297 | ) { 298 | ui.style_mut().visuals.extreme_bg_color = ui.visuals().extreme_bg_color; 299 | } 300 | 301 | fn syntax_highlighting( 302 | &self, 303 | _cache: &mut CommonMarkCache, 304 | _options: &CommonMarkOptions, 305 | extension: &str, 306 | ui: &Ui, 307 | text: &str, 308 | ) -> egui::text::LayoutJob { 309 | simple_highlighting(ui, text, extension) 310 | } 311 | } 312 | 313 | #[cfg(feature = "better_syntax_highlighting")] 314 | impl CodeBlock { 315 | fn pre_syntax_highlighting( 316 | cache: &mut CommonMarkCache, 317 | options: &CommonMarkOptions, 318 | ui: &mut Ui, 319 | ) { 320 | let curr_theme = cache.curr_theme(ui, options); 321 | let style = ui.style_mut(); 322 | 323 | style.visuals.extreme_bg_color = curr_theme 324 | .settings 325 | .background 326 | .map(syntect_color_to_egui) 327 | .unwrap_or(style.visuals.extreme_bg_color); 328 | 329 | if let Some(color) = curr_theme.settings.selection_foreground { 330 | style.visuals.selection.bg_fill = syntect_color_to_egui(color); 331 | } 332 | } 333 | 334 | fn syntax_highlighting( 335 | &self, 336 | cache: &CommonMarkCache, 337 | options: &CommonMarkOptions, 338 | extension: &str, 339 | ui: &Ui, 340 | text: &str, 341 | ) -> egui::text::LayoutJob { 342 | if let Some(syntax) = cache.ps.find_syntax_by_extension(extension) { 343 | let mut job = egui::text::LayoutJob::default(); 344 | let mut h = HighlightLines::new(syntax, cache.curr_theme(ui, options)); 345 | 346 | for line in LinesWithEndings::from(text) { 347 | let ranges = h.highlight_line(line, &cache.ps).unwrap(); 348 | for v in ranges { 349 | let front = v.0.foreground; 350 | job.append( 351 | v.1, 352 | 0.0, 353 | egui::TextFormat::simple( 354 | TextStyle::Monospace.resolve(ui.style()), 355 | syntect_color_to_egui(front), 356 | ), 357 | ); 358 | } 359 | } 360 | 361 | job 362 | } else { 363 | simple_highlighting(ui, text, extension) 364 | } 365 | } 366 | } 367 | 368 | fn simple_highlighting(ui: &Ui, text: &str, extension: &str) -> egui::text::LayoutJob { 369 | egui_extras::syntax_highlighting::highlight( 370 | ui.ctx(), 371 | ui.style(), 372 | &egui_extras::syntax_highlighting::CodeTheme::from_style(ui.style()), 373 | text, 374 | extension, 375 | ) 376 | } 377 | 378 | fn plain_highlighting(ui: &Ui, text: &str) -> egui::text::LayoutJob { 379 | let mut job = egui::text::LayoutJob::default(); 380 | job.append( 381 | text, 382 | 0.0, 383 | egui::TextFormat::simple( 384 | TextStyle::Monospace.resolve(ui.style()), 385 | ui.style().visuals.text_color(), 386 | ), 387 | ); 388 | job 389 | } 390 | 391 | #[cfg(feature = "better_syntax_highlighting")] 392 | fn syntect_color_to_egui(color: syntect::highlighting::Color) -> egui::Color32 { 393 | egui::Color32::from_rgb(color.r, color.g, color.b) 394 | } 395 | 396 | #[cfg(feature = "better_syntax_highlighting")] 397 | fn default_theme(ui: &Ui) -> &str { 398 | if ui.style().visuals.dark_mode { 399 | DEFAULT_THEME_DARK 400 | } else { 401 | DEFAULT_THEME_LIGHT 402 | } 403 | } 404 | 405 | /// A cache used for storing content such as images. 406 | #[derive(Debug)] 407 | pub struct CommonMarkCache { 408 | // Everything stored in `CommonMarkCache` must take into account that 409 | // the cache is for multiple `CommonMarkviewer`s with different source_ids. 410 | #[cfg(feature = "better_syntax_highlighting")] 411 | ps: SyntaxSet, 412 | 413 | #[cfg(feature = "better_syntax_highlighting")] 414 | ts: ThemeSet, 415 | 416 | link_hooks: HashMap, 417 | 418 | scroll: HashMap, 419 | pub(self) has_installed_loaders: bool, 420 | } 421 | 422 | #[allow(clippy::derivable_impls)] 423 | impl Default for CommonMarkCache { 424 | fn default() -> Self { 425 | Self { 426 | #[cfg(feature = "better_syntax_highlighting")] 427 | ps: SyntaxSet::load_defaults_newlines(), 428 | #[cfg(feature = "better_syntax_highlighting")] 429 | ts: ThemeSet::load_defaults(), 430 | link_hooks: HashMap::new(), 431 | scroll: Default::default(), 432 | has_installed_loaders: false, 433 | } 434 | } 435 | } 436 | 437 | impl CommonMarkCache { 438 | #[cfg(feature = "better_syntax_highlighting")] 439 | pub fn add_syntax_from_folder(&mut self, path: &str) { 440 | let mut builder = self.ps.clone().into_builder(); 441 | let _ = builder.add_from_folder(path, true); 442 | self.ps = builder.build(); 443 | } 444 | 445 | #[cfg(feature = "better_syntax_highlighting")] 446 | pub fn add_syntax_from_str(&mut self, s: &str, fallback_name: Option<&str>) { 447 | let mut builder = self.ps.clone().into_builder(); 448 | let _ = SyntaxDefinition::load_from_str(s, true, fallback_name).map(|d| builder.add(d)); 449 | self.ps = builder.build(); 450 | } 451 | 452 | #[cfg(feature = "better_syntax_highlighting")] 453 | /// Add more color themes for code blocks(.tmTheme files). Set the color theme with 454 | /// [`syntax_theme_dark`](CommonMarkViewer::syntax_theme_dark) and 455 | /// [`syntax_theme_light`](CommonMarkViewer::syntax_theme_light) 456 | pub fn add_syntax_themes_from_folder( 457 | &mut self, 458 | path: impl AsRef, 459 | ) -> Result<(), syntect::LoadingError> { 460 | self.ts.add_from_folder(path) 461 | } 462 | 463 | #[cfg(feature = "better_syntax_highlighting")] 464 | /// Add color theme for code blocks(.tmTheme files). Set the color theme with 465 | /// [`syntax_theme_dark`](CommonMarkViewer::syntax_theme_dark) and 466 | /// [`syntax_theme_light`](CommonMarkViewer::syntax_theme_light) 467 | pub fn add_syntax_theme_from_bytes( 468 | &mut self, 469 | name: impl Into, 470 | bytes: &[u8], 471 | ) -> Result<(), syntect::LoadingError> { 472 | let mut cursor = std::io::Cursor::new(bytes); 473 | self.ts 474 | .themes 475 | .insert(name.into(), ThemeSet::load_from_reader(&mut cursor)?); 476 | Ok(()) 477 | } 478 | 479 | /// Clear the cache for all scrollable elements 480 | pub fn clear_scrollable(&mut self) { 481 | self.scroll.clear(); 482 | } 483 | 484 | /// Clear the cache for a specific scrollable viewer. Returns false if the 485 | /// id was not in the cache. 486 | pub fn clear_scrollable_with_id(&mut self, source_id: impl std::hash::Hash) -> bool { 487 | self.scroll.remove(&egui::Id::new(source_id)).is_some() 488 | } 489 | /// If the user clicks on a link in the markdown render that has `name` as a link. The hook 490 | /// specified with this method will be set to true. It's status can be acquired 491 | /// with [`get_link_hook`](Self::get_link_hook). Be aware that all hook state is reset once 492 | /// [`CommonMarkViewer::show`] gets called 493 | /// 494 | /// # Why use link hooks 495 | /// 496 | /// egui provides a method for checking links afterwards so why use this instead? 497 | /// 498 | /// ```rust 499 | /// # use egui::__run_test_ctx; 500 | /// # __run_test_ctx(|ctx| { 501 | /// ctx.output_mut(|o| o.open_url.is_some()); 502 | /// # }); 503 | /// ``` 504 | /// 505 | /// The main difference is that link hooks allows egui_commonmark to check for link hooks 506 | /// while rendering. Normally when hovering over a link, egui_commonmark will display the full 507 | /// url. With link hooks this feature is disabled, but to do that all hooks must be known. 508 | // Works when displayed through egui_commonmark 509 | #[allow(rustdoc::broken_intra_doc_links)] 510 | pub fn add_link_hook>(&mut self, name: S) { 511 | self.link_hooks.insert(name.into(), false); 512 | } 513 | 514 | /// Returns None if the link hook could not be found. Returns the last known status of the 515 | /// hook otherwise. 516 | pub fn remove_link_hook(&mut self, name: &str) -> Option { 517 | self.link_hooks.remove(name) 518 | } 519 | 520 | /// Get status of link. Returns true if it was clicked 521 | pub fn get_link_hook(&self, name: &str) -> Option { 522 | self.link_hooks.get(name).copied() 523 | } 524 | 525 | /// Remove all link hooks 526 | pub fn link_hooks_clear(&mut self) { 527 | self.link_hooks.clear(); 528 | } 529 | 530 | /// All link hooks 531 | pub fn link_hooks(&self) -> &HashMap { 532 | &self.link_hooks 533 | } 534 | 535 | /// Raw access to link hooks 536 | pub fn link_hooks_mut(&mut self) -> &mut HashMap { 537 | &mut self.link_hooks 538 | } 539 | 540 | /// Set all link hooks to false 541 | fn deactivate_link_hooks(&mut self) { 542 | for v in self.link_hooks.values_mut() { 543 | *v = false; 544 | } 545 | } 546 | 547 | #[cfg(feature = "better_syntax_highlighting")] 548 | fn curr_theme(&self, ui: &Ui, options: &CommonMarkOptions) -> &Theme { 549 | self.ts 550 | .themes 551 | .get(options.curr_theme(ui)) 552 | // Since we have called load_defaults, the default theme *should* always be available.. 553 | .unwrap_or_else(|| &self.ts.themes[default_theme(ui)]) 554 | } 555 | } 556 | 557 | pub fn scroll_cache<'a>(cache: &'a mut CommonMarkCache, id: &egui::Id) -> &'a mut ScrollableCache { 558 | if !cache.scroll.contains_key(id) { 559 | cache.scroll.insert(*id, Default::default()); 560 | } 561 | cache.scroll.get_mut(id).unwrap() 562 | } 563 | 564 | /// Should be called before any rendering 565 | pub fn prepare_show(cache: &mut CommonMarkCache, ctx: &egui::Context) { 566 | if !cache.has_installed_loaders { 567 | // Even though the install function can be called multiple times, its not the cheapest 568 | // so we ensure that we only call it once. 569 | // This could be done at the creation of the cache, however it is better to keep the 570 | // cache free from egui's Ui and Context types as this allows it to be created before 571 | // any egui instances. It also keeps the API similar to before the introduction of the 572 | // image loaders. 573 | #[cfg(feature = "embedded_image")] 574 | crate::data_url_loader::install_loader(ctx); 575 | 576 | egui_extras::install_image_loaders(ctx); 577 | cache.has_installed_loaders = true; 578 | } 579 | 580 | cache.deactivate_link_hooks(); 581 | } 582 | -------------------------------------------------------------------------------- /egui_commonmark_backend/src/pulldown.rs: -------------------------------------------------------------------------------- 1 | use crate::alerts::*; 2 | use egui::{Pos2, Vec2}; 3 | use pulldown_cmark::Options; 4 | use std::ops::Range; 5 | 6 | #[derive(Default, Debug)] 7 | pub struct ScrollableCache { 8 | pub available_size: Vec2, 9 | pub page_size: Option, 10 | pub split_points: Vec<(usize, Pos2, Pos2)>, 11 | } 12 | 13 | pub type EventIteratorItem<'e> = (usize, (pulldown_cmark::Event<'e>, Range)); 14 | 15 | /// Parse events until a desired end tag is reached or no more events are found. 16 | /// This is needed for multiple events that must be rendered inside a single widget 17 | pub fn delayed_events<'e>( 18 | events: &mut impl Iterator>, 19 | end_at: impl Fn(pulldown_cmark::TagEnd) -> bool, 20 | ) -> Vec<(pulldown_cmark::Event<'e>, Range)> { 21 | let mut curr_event = events.next(); 22 | let mut total_events = Vec::new(); 23 | loop { 24 | if let Some(event) = curr_event.take() { 25 | total_events.push(event.1.clone()); 26 | if let (_, (pulldown_cmark::Event::End(tag), _range)) = event { 27 | if end_at(tag) { 28 | return total_events; 29 | } 30 | } 31 | } else { 32 | return total_events; 33 | } 34 | 35 | curr_event = events.next(); 36 | } 37 | } 38 | 39 | pub fn delayed_events_list_item<'e>( 40 | events: &mut impl Iterator>, 41 | ) -> Vec<(pulldown_cmark::Event<'e>, Range)> { 42 | let mut curr_event = events.next(); 43 | let mut total_events = Vec::new(); 44 | loop { 45 | if let Some(event) = curr_event.take() { 46 | total_events.push(event.1.clone()); 47 | if let (_, (pulldown_cmark::Event::End(pulldown_cmark::TagEnd::Item), _range)) = event { 48 | return total_events; 49 | } 50 | 51 | if let (_, (pulldown_cmark::Event::Start(pulldown_cmark::Tag::List(_)), _range)) = event 52 | { 53 | return total_events; 54 | } 55 | } else { 56 | return total_events; 57 | } 58 | 59 | curr_event = events.next(); 60 | } 61 | } 62 | 63 | type Column<'e> = Vec<(pulldown_cmark::Event<'e>, Range)>; 64 | type Row<'e> = Vec>; 65 | 66 | pub struct Table<'e> { 67 | pub header: Row<'e>, 68 | pub rows: Vec>, 69 | } 70 | 71 | fn parse_row<'e>( 72 | events: &mut impl Iterator, Range)>, 73 | ) -> Vec> { 74 | let mut row = Vec::new(); 75 | let mut column = Vec::new(); 76 | 77 | for (e, src_span) in events.by_ref() { 78 | if let pulldown_cmark::Event::End(pulldown_cmark::TagEnd::TableCell) = e { 79 | row.push(column); 80 | column = Vec::new(); 81 | } 82 | 83 | if let pulldown_cmark::Event::End(pulldown_cmark::TagEnd::TableHead) = e { 84 | break; 85 | } 86 | 87 | if let pulldown_cmark::Event::End(pulldown_cmark::TagEnd::TableRow) = e { 88 | break; 89 | } 90 | 91 | column.push((e, src_span)); 92 | } 93 | 94 | row 95 | } 96 | 97 | pub fn parse_table<'e>(events: &mut impl Iterator>) -> Table<'e> { 98 | let mut all_events = delayed_events(events, |end| matches!(end, pulldown_cmark::TagEnd::Table)) 99 | .into_iter() 100 | .peekable(); 101 | 102 | let header = parse_row(&mut all_events); 103 | 104 | let mut rows = Vec::new(); 105 | while all_events.peek().is_some() { 106 | let row = parse_row(&mut all_events); 107 | rows.push(row); 108 | } 109 | 110 | Table { header, rows } 111 | } 112 | 113 | /// try to parse events as an alert quote block. This ill modify the events 114 | /// to remove the parsed text that should not be rendered. 115 | /// Assumes that the first element is a Paragraph 116 | pub fn parse_alerts<'a>( 117 | alerts: &'a AlertBundle, 118 | events: &mut Vec<(pulldown_cmark::Event<'_>, Range)>, 119 | ) -> Option<&'a Alert> { 120 | // no point in parsing if there are no alerts to render 121 | if !alerts.is_empty() { 122 | let mut alert_ident = "".to_owned(); 123 | let mut alert_ident_ends_at = 0; 124 | let mut has_extra_line = false; 125 | 126 | for (i, (e, _src_span)) in events.iter().enumerate() { 127 | if let pulldown_cmark::Event::End(_) = e { 128 | // > [!TIP] 129 | // > 130 | // > Detect the first paragraph 131 | // In this case the next text will be within a paragraph so it is better to remove 132 | // the entire paragraph 133 | alert_ident_ends_at = i; 134 | has_extra_line = true; 135 | break; 136 | } 137 | 138 | if let pulldown_cmark::Event::SoftBreak = e { 139 | // > [!NOTE] 140 | // > this is valid and will produce a soft break 141 | alert_ident_ends_at = i; 142 | break; 143 | } 144 | 145 | if let pulldown_cmark::Event::HardBreak = e { 146 | // > [!NOTE] 147 | // > this is valid and will produce a hard break 148 | alert_ident_ends_at = i; 149 | break; 150 | } 151 | 152 | if let pulldown_cmark::Event::Text(text) = e { 153 | alert_ident += text; 154 | } 155 | } 156 | 157 | let alert = try_get_alert(alerts, &alert_ident); 158 | 159 | if alert.is_some() { 160 | // remove the text that identifies it as an alert so that it won't end up in the 161 | // render 162 | // 163 | // FIMXE: performance improvement potential 164 | if has_extra_line { 165 | for _ in 0..=alert_ident_ends_at { 166 | events.remove(0); 167 | } 168 | } else { 169 | for _ in 0..alert_ident_ends_at { 170 | // the first element must be kept as it _should_ be Paragraph 171 | events.remove(1); 172 | } 173 | } 174 | } 175 | 176 | alert 177 | } else { 178 | None 179 | } 180 | } 181 | 182 | /// Supported pulldown_cmark options 183 | #[inline] 184 | pub fn parser_options() -> Options { 185 | Options::ENABLE_TABLES 186 | | Options::ENABLE_TASKLISTS 187 | | Options::ENABLE_STRIKETHROUGH 188 | | Options::ENABLE_FOOTNOTES 189 | | Options::ENABLE_DEFINITION_LIST 190 | } 191 | -------------------------------------------------------------------------------- /egui_commonmark_macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "egui_commonmark_macros" 3 | authors = ["Erlend Walstad"] 4 | 5 | version.workspace = true 6 | edition.workspace = true 7 | license.workspace = true 8 | rust-version.workspace = true 9 | repository.workspace = true 10 | 11 | description = "Embed markdown directly into the binary as egui widgets" 12 | keywords = ["commonmark", "egui"] 13 | categories = ["gui"] 14 | readme = "README.md" 15 | documentation = "https://docs.rs/egui_commonmark_macros" 16 | include = ["src/**/*.rs", "LICENSE-MIT", "LICENSE-APACHE", "Cargo.toml"] 17 | 18 | 19 | [lib] 20 | proc-macro = true 21 | 22 | [dependencies] 23 | syn = "2.0" 24 | quote = { version = "1.0", default-features = false } 25 | proc-macro2 = { version = "1.0", default-features = false } 26 | 27 | pulldown-cmark = { workspace = true, default-features = false } 28 | 29 | egui_commonmark_backend = { workspace = true } 30 | egui = { workspace = true } 31 | 32 | document-features = { workspace = true, optional = true } 33 | 34 | # Dependency resolution is hard... 35 | proc-macro-crate = "3.1" 36 | 37 | [dev-dependencies] 38 | trybuild = "1.0" 39 | 40 | [features] 41 | # Internal Debugging tool only! 42 | dump-macro = [] 43 | 44 | ## Enable tracking of markdown files to recompile when their content changes 45 | nightly = [] 46 | 47 | [package.metadata.docs.rs] 48 | features = ["document-features"] 49 | -------------------------------------------------------------------------------- /egui_commonmark_macros/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /egui_commonmark_macros/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /egui_commonmark_macros/README.md: -------------------------------------------------------------------------------- 1 | # A commonmark viewer for [egui](https://github.com/emilk/egui) 2 | 3 | [![Crate](https://img.shields.io/crates/v/egui_commonmark_macros.svg)](https://crates.io/crates/egui_commonmark_macros) 4 | [![Documentation](https://docs.rs/egui_commonmark_macros/badge.svg)](https://docs.rs/egui_commonmark_macros) 5 | 6 | showcase 7 | 8 | This crate is `egui_commonmark`'s compile time variant. It is recommended to use 9 | this crate through `egui_commonmark` by enabling the `macros` feature. 10 | 11 | 12 | ## Usage 13 | 14 | In Cargo.toml: 15 | 16 | ```toml 17 | egui_commonmark = "0.20" 18 | # Specify what image formats you want to use 19 | image = { version = "0.25", default-features = false, features = ["png"] } 20 | ``` 21 | 22 | ### Example 23 | 24 | ```rust 25 | use egui_commonmark::{CommonMarkCache, commonmark}; 26 | let mut cache = CommonMarkCache::default(); 27 | let _response = commonmark!(ui, &mut cache, "# ATX Heading Level 1"); 28 | ``` 29 | 30 | Alternatively you can embed a file 31 | 32 | ### Example 33 | 34 | ```rust 35 | use egui_commonmark::{CommonMarkCache, commonmark_str}; 36 | let mut cache = CommonMarkCache::default(); 37 | commonmark_str!(ui, &mut cache, "content.md"); 38 | ``` 39 | 40 | ## License 41 | 42 | Licensed under either of 43 | 44 | * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 45 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 46 | 47 | at your option. 48 | -------------------------------------------------------------------------------- /egui_commonmark_macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Compile time evaluation of markdown that generates egui widgets 2 | //! 3 | //! It is recommended to use this crate through the parent crate 4 | //! [egui_commonmark](https://docs.rs/crate/egui_commonmark/latest). 5 | //! If you for some reason don't want to use it you must also import 6 | //! [egui_commonmark_backend](https://docs.rs/crate/egui_commonmark_backend/latest) 7 | //! directly from your crate to get access to `CommonMarkCache` and internals that 8 | //! the macros require for the final generated code. 9 | //! 10 | //! ## API 11 | //! ### Embedding markdown text directly 12 | //! 13 | //! The macro has the following format: 14 | //! 15 | //! commonmark!(ui, cache, text); 16 | //! 17 | //! #### Example 18 | //! 19 | //! ``` 20 | //! # // If used through egui_commonmark the backend crate does not need to be relied upon 21 | //! # use egui_commonmark_backend::CommonMarkCache; 22 | //! # use egui_commonmark_macros::commonmark; 23 | //! # egui::__run_test_ui(|ui| { 24 | //! let mut cache = CommonMarkCache::default(); 25 | //! let _response = commonmark!(ui, &mut cache, "# ATX Heading Level 1"); 26 | //! # }); 27 | //! ``` 28 | //! 29 | //! As you can see it also returns a response like most other egui widgets. 30 | //! 31 | //! ### Embedding markdown file 32 | //! 33 | //! The macro has the exact same format as the `commonmark!` macro: 34 | //! 35 | //! commonmark_str!(ui, cache, file_path); 36 | //! 37 | //! #### Example 38 | //! 39 | // Unfortunately can't depend on an actual file in the doc test so it must be 40 | // disabled 41 | //! ```rust,ignore 42 | //! # use egui_commonmark_backend::CommonMarkCache; 43 | //! # use egui_commonmark_macros::commonmark_str; 44 | //! # egui::__run_test_ui(|ui| { 45 | //! let mut cache = CommonMarkCache::default(); 46 | //! commonmark_str!(ui, &mut cache, "foo.md"); 47 | //! # }); 48 | //! ``` 49 | //! 50 | //! One drawback is that the file cannot be tracked by rust on stable so the 51 | //! program won't recompile if you only change the content of the file. To 52 | //! work around this you can use a nightly compiler and enable the 53 | //! `nightly` feature when iterating on your markdown files. 54 | //! 55 | //! ## Limitations 56 | //! 57 | //! Compared to it's runtime counterpart egui_commonmark it currently does not 58 | //! offer customization. This is something that will be addressed eventually once 59 | //! a good API has been chosen. 60 | //! 61 | //! ## What this crate is not 62 | //! 63 | //! This crate does not have as a goal to make widgets that can be interacted with 64 | //! through code. 65 | //! 66 | //! ```rust,ignore 67 | //! let ... = commonmark!(ui, &mut cache, "- [ ] Task List"); 68 | //! task_list.set_checked(true); // No !! 69 | //! ``` 70 | //! 71 | //! For that you should fall back to normal egui widgets 72 | #![cfg_attr(feature = "document-features", doc = "# Features")] 73 | #![cfg_attr(feature = "document-features", doc = document_features::document_features!())] 74 | #![cfg_attr(feature = "nightly", feature(track_path))] 75 | 76 | mod generator; 77 | use generator::*; 78 | 79 | use quote::quote_spanned; 80 | use syn::parse::{Parse, ParseStream, Result}; 81 | use syn::{parse_macro_input, Expr, LitStr, Token}; 82 | 83 | struct Parameters { 84 | ui: Expr, 85 | cache: Expr, 86 | markdown: LitStr, 87 | } 88 | 89 | impl Parse for Parameters { 90 | fn parse(input: ParseStream) -> Result { 91 | let ui: Expr = input.parse()?; 92 | input.parse::()?; 93 | let cache: Expr = input.parse()?; 94 | input.parse::()?; 95 | let markdown: LitStr = input.parse()?; 96 | 97 | Ok(Parameters { 98 | ui, 99 | cache, 100 | markdown, 101 | }) 102 | } 103 | } 104 | 105 | fn commonmark_impl(ui: Expr, cache: Expr, text: String) -> proc_macro2::TokenStream { 106 | let stream = CommonMarkViewerInternal::new().show(ui, cache, &text); 107 | 108 | #[cfg(feature = "dump-macro")] 109 | { 110 | // Wrap within a function to allow rustfmt to format it 111 | println!("fn main() {{"); 112 | println!("{}", stream.to_string()); 113 | println!("}}"); 114 | } 115 | 116 | // false positive due to feature gate 117 | #[allow(clippy::let_and_return)] 118 | stream 119 | } 120 | 121 | #[proc_macro] 122 | pub fn commonmark(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 123 | let Parameters { 124 | ui, 125 | cache, 126 | markdown, 127 | } = parse_macro_input!(input as Parameters); 128 | 129 | commonmark_impl(ui, cache, markdown.value()).into() 130 | } 131 | 132 | #[proc_macro] 133 | pub fn commonmark_str(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 134 | let Parameters { 135 | ui, 136 | cache, 137 | markdown, 138 | } = parse_macro_input!(input as Parameters); 139 | 140 | let path = markdown.value(); 141 | #[cfg(feature = "nightly")] 142 | { 143 | // Tell rust to track the file so that the macro will regenerate when the 144 | // file changes 145 | proc_macro::tracked_path::path(&path); 146 | } 147 | 148 | let Ok(md) = std::fs::read_to_string(path) else { 149 | return quote_spanned!(markdown.span()=> 150 | compile_error!("Could not find markdown file"); 151 | ) 152 | .into(); 153 | }; 154 | 155 | commonmark_impl(ui, cache, md).into() 156 | } 157 | 158 | fn resolve_backend_crate_import() -> proc_macro2::TokenStream { 159 | // The purpose of this is to ensure that when used through egui_commonmark 160 | // the generated code can always find egui_commonmark_backend without the 161 | // user having to import themselves. 162 | // 163 | // There are other ways to do this that does not depend on an external crate 164 | // such as exposing a feature flag in this crate that egui_commonmark can set. 165 | // This works for users, however it is a pain to use in this workspace as 166 | // the macro tests won't work when run from the workspace directory. So instead 167 | // they must be run from this crate's workspace. I don't want to rely on that mess 168 | // so this is the solution. I have also tried some other solutions with no success 169 | // or they had drawbacks that I did not like. 170 | // 171 | // With all that said the resolution is the following: 172 | // 173 | // Try egui_commonmark_backend first. This ensures that the tests will run from 174 | // the main workspace despite egui_commonmark being present. However if only 175 | // egui_commonmark is present then a `use egui_commonmark::egui_commonmark_backend;` 176 | // will be inserted into the generated code. 177 | // 178 | // If none of that work's then the user is missing some crates 179 | 180 | let backend_crate = proc_macro_crate::crate_name("egui_commonmark_backend"); 181 | let main_crate = proc_macro_crate::crate_name("egui_commonmark"); 182 | 183 | if backend_crate.is_ok() { 184 | proc_macro2::TokenStream::new() 185 | } else if let Ok(found_crate) = main_crate { 186 | let crate_name = match found_crate { 187 | proc_macro_crate::FoundCrate::Itself => return proc_macro2::TokenStream::new(), 188 | proc_macro_crate::FoundCrate::Name(name) => name, 189 | }; 190 | 191 | let crate_name_lit = proc_macro2::Ident::new(&crate_name, proc_macro2::Span::call_site()); 192 | quote::quote!( 193 | use #crate_name_lit::egui_commonmark_backend; 194 | ) 195 | } else { 196 | proc_macro2::TokenStream::new() 197 | } 198 | } 199 | 200 | #[test] 201 | fn tests() { 202 | let t = trybuild::TestCases::new(); 203 | t.pass("tests/pass/*.rs"); 204 | t.compile_fail("tests/fail/*.rs"); 205 | } 206 | -------------------------------------------------------------------------------- /egui_commonmark_macros/tests/fail/commonmark_str_not_found.rs: -------------------------------------------------------------------------------- 1 | use egui::__run_test_ui; 2 | use egui_commonmark_macros::commonmark_str; 3 | 4 | // Check that it fails to compile when it is not able to find a file 5 | fn main() { 6 | let mut cache = egui_commonmark_backend::CommonMarkCache::default(); 7 | __run_test_ui(|ui| { 8 | commonmark_str!(ui, &mut cache, "foo.md"); 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /egui_commonmark_macros/tests/fail/commonmark_str_not_found.stderr: -------------------------------------------------------------------------------- 1 | error: Could not find markdown file 2 | --> tests/fail/commonmark_str_not_found.rs:8:41 3 | | 4 | 8 | commonmark_str!(ui, &mut cache, "foo.md"); 5 | | ^^^^^^^^ 6 | -------------------------------------------------------------------------------- /egui_commonmark_macros/tests/fail/incorrect_immutable.rs: -------------------------------------------------------------------------------- 1 | use egui_commonmark_macros::commonmark; 2 | 3 | // Ensure that the error message is sane 4 | fn main() { 5 | let mut cache = egui_commonmark_backend::CommonMarkCache::default(); 6 | egui::__run_test_ui(|ui| { 7 | commonmark!(ui, &cache, "# Hello"); 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /egui_commonmark_macros/tests/fail/incorrect_immutable.stderr: -------------------------------------------------------------------------------- 1 | error[E0308]: mismatched types 2 | --> tests/fail/incorrect_immutable.rs:7:25 3 | | 4 | 7 | commonmark!(ui, &cache, "# Hello"); 5 | | ----------------^^^^^^------------ 6 | | | | 7 | | | types differ in mutability 8 | | arguments to this function are incorrect 9 | | 10 | = note: expected mutable reference `&mut CommonMarkCache` 11 | found reference `&CommonMarkCache` 12 | note: function defined here 13 | --> $WORKSPACE/egui_commonmark_backend/src/misc.rs 14 | | 15 | | pub fn prepare_show(cache: &mut CommonMarkCache, ctx: &egui::Context) { 16 | | ^^^^^^^^^^^^ 17 | -------------------------------------------------------------------------------- /egui_commonmark_macros/tests/fail/incorrect_type.rs: -------------------------------------------------------------------------------- 1 | use egui_commonmark_macros::commonmark; 2 | 3 | // Ensure that the error message is sane 4 | fn main() { 5 | let mut cache = egui_commonmark_backend::CommonMarkCache::default(); 6 | let x = 3; 7 | commonmark!(x, &mut cache, "# Hello"); 8 | } 9 | -------------------------------------------------------------------------------- /egui_commonmark_macros/tests/fail/incorrect_type.stderr: -------------------------------------------------------------------------------- 1 | error[E0308]: mismatched types 2 | --> tests/fail/incorrect_type.rs:7:17 3 | | 4 | 7 | commonmark!(x, &mut cache, "# Hello"); 5 | | ------------^------------------------ 6 | | | | 7 | | | expected `&mut Ui`, found integer 8 | | expected due to this 9 | -------------------------------------------------------------------------------- /egui_commonmark_macros/tests/file.md: -------------------------------------------------------------------------------- 1 | # Hello World 2 | 3 | ------------- 4 | __a__ **b** 5 | -------------------------------------------------------------------------------- /egui_commonmark_macros/tests/pass/book.rs: -------------------------------------------------------------------------------- 1 | use egui::__run_test_ui; 2 | use egui_commonmark_macros::commonmark_str; 3 | 4 | // Testing all the different examples should give fairly good coverage 5 | fn main() { 6 | __run_test_ui(|ui| { 7 | let mut cache = egui_commonmark_backend::CommonMarkCache::default(); 8 | commonmark_str!( 9 | ui, 10 | &mut cache, 11 | "../../../../egui_commonmark/examples/markdown/hello_world.md" 12 | ); 13 | 14 | commonmark_str!( 15 | ui, 16 | &mut cache, 17 | "../../../../egui_commonmark/examples/markdown/headers.md" 18 | ); 19 | 20 | commonmark_str!( 21 | ui, 22 | &mut cache, 23 | "../../../../egui_commonmark/examples/markdown/lists.md" 24 | ); 25 | 26 | commonmark_str!( 27 | ui, 28 | &mut cache, 29 | "../../../../egui_commonmark/examples/markdown/code-blocks.md" 30 | ); 31 | 32 | commonmark_str!( 33 | ui, 34 | &mut cache, 35 | "../../../../egui_commonmark/examples/markdown/blockquotes.md" 36 | ); 37 | 38 | commonmark_str!( 39 | ui, 40 | &mut cache, 41 | "../../../../egui_commonmark/examples/markdown/tables.md" 42 | ); 43 | commonmark_str!( 44 | ui, 45 | &mut cache, 46 | "../../../../egui_commonmark/examples/markdown/definition_list.md" 47 | ); 48 | }); 49 | } 50 | -------------------------------------------------------------------------------- /egui_commonmark_macros/tests/pass/commonmark.rs: -------------------------------------------------------------------------------- 1 | use egui::__run_test_ui; 2 | use egui_commonmark_macros::commonmark; 3 | 4 | // Check a simple case and ensure that it returns a reponse 5 | fn main() { 6 | __run_test_ui(|ui| { 7 | let mut cache = egui_commonmark_backend::CommonMarkCache::default(); 8 | let _response: egui::InnerResponse<()> = commonmark!(ui, &mut cache, "# Hello, World"); 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /egui_commonmark_macros/tests/pass/commonmark_str.rs: -------------------------------------------------------------------------------- 1 | use egui::__run_test_ui; 2 | use egui_commonmark_macros::commonmark_str; 3 | 4 | // Check a simple case and ensure that it returns a reponse 5 | fn main() { 6 | __run_test_ui(|ui| { 7 | let mut cache = egui_commonmark_backend::CommonMarkCache::default(); 8 | let _response: egui::InnerResponse<()> = commonmark_str!( 9 | ui, 10 | &mut cache, 11 | "../../../../egui_commonmark_macros/tests/file.md" 12 | ); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /egui_commonmark_macros/tests/pass/ui_hygiene.rs: -------------------------------------------------------------------------------- 1 | use egui::__run_test_ui; 2 | use egui_commonmark_macros::commonmark; 3 | 4 | // Check hygiene of the ui expression 5 | fn main() { 6 | __run_test_ui(|ui| { 7 | let mut cache = egui_commonmark_backend::CommonMarkCache::default(); 8 | egui::ScrollArea::vertical().show(ui, |ui| { 9 | egui::Frame::none().show(ui, |not_named_ui| { 10 | commonmark!(not_named_ui, &mut cache, "# Hello, World"); 11 | }) 12 | }); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.81.0" 3 | components = ["rustfmt", "clippy"] 4 | --------------------------------------------------------------------------------