|
| 1 | +- Feature Name: `rustdoc_texmath` |
| 2 | +- Start Date: (fill me in with today's date, YYYY-MM-DD) |
| 3 | +- RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/3958) |
| 4 | +- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000) |
| 5 | + |
| 6 | +## Summary |
| 7 | +[summary]: #summary |
| 8 | + |
| 9 | +Add support for the de facto standard TeX-math-in-markdown syntax to Rustdoc. It’s currently implemented using the [math-core][] library, which generates MathML Core and is restricted to the subset of LaTeX that can be implemented that way. If you use unsupported syntax, you get a compiler warning from rustdoc. |
| 10 | + |
| 11 | +[math-core]: https://docs.rs/math-core/latest/math_core/ |
| 12 | + |
| 13 | +## Motivation |
| 14 | +[motivation]: #motivation |
| 15 | + |
| 16 | +It would be nice if we could write complex equations in our docs. |
| 17 | +We know that there's demand for this feature, |
| 18 | +first of all because people have [asked for it][internals thread], |
| 19 | +but mostly because of [crates that did it themselves][] by loading [katex.js][] with inline HTML. |
| 20 | + |
| 21 | +As far as I know, this is the most popular way of doing that: |
| 22 | + |
| 23 | + [package.metadata.docs.rs] |
| 24 | + rustdoc-args = ["--html-in-header", "katex-header.html", "--cfg", "docsrs"] |
| 25 | + |
| 26 | +Because only docs.rs reads that directive, |
| 27 | +local `cargo doc` and non-Rustdoc doc readers won't see it. |
| 28 | +There is a way to make it work in `cargo doc`, |
| 29 | +but it [seems to be less popular][include hack]. |
| 30 | + |
| 31 | +Fixing this bug and making it easier to build self-contained docs is |
| 32 | +the main motivation for adding built-in support for math syntax to Rustdoc, |
| 33 | +but there are a few other quality of life improvements that come with this feature: |
| 34 | + |
| 35 | +- We can report math syntax errors on the CLI, just like we do for intra-doc links. |
| 36 | +- We can render math in the resulting web page without JavaScript. |
| 37 | + No flash of unstyled content or blocking scripts. |
| 38 | +- Built-in TeX math doesn't require double-escaping, because the Markdown parser knows about math, |
| 39 | + and lets you backslash escape the dollar sign to disable it. |
| 40 | +- Cross-crate inlining works. |
| 41 | + |
| 42 | +[internals thread]: https://internals.rust-lang.org/t/adding-latex-support-to-rustdoc/23858 |
| 43 | +[crates that did it themselves]: https://github.com/search?q=rustdoc-args+%3D+%5B%22--html-in-header%22%2C+%22katex-header.html%22%2C+%22--cfg%22%2C+%22docsrs%22%5D+language%3Atoml&type=code |
| 44 | +[katex.js]: https://katex.org/ |
| 45 | +[include hack]: https://github.com/search?q=%23%21%5Bdoc+%3D+include_str%21%28%22katex.html%22%29%5D+language%3Arust&type=code |
| 46 | + |
| 47 | +## Guide-level explanation |
| 48 | +[guide-level-explanation]: #guide-level-explanation |
| 49 | + |
| 50 | +### How to enable |
| 51 | + |
| 52 | +To enable `$\TeX$` math syntax in rustdoc, add this line to your crate root. |
| 53 | + |
| 54 | + #![doc(math_syntax)] |
| 55 | + |
| 56 | +In a future edition, we may enable it by default. If you need to turn it off, add this line to your crate root. |
| 57 | + |
| 58 | + #![doc(no_math_syntax)] |
| 59 | + |
| 60 | +When this feature is enabled, equations are wrapped in single or double `$` dollar signs. |
| 61 | + |
| 62 | + $$\sum_{i=0}^N x_i$$ |
| 63 | + |
| 64 | +The result looks like this: |
| 65 | + |
| 66 | +>  |
| 67 | +
|
| 68 | +A detailed comparison between our syntax and KaTeX's can be found |
| 69 | +[here](https://tmke8.github.io/math-core/comparison.html). |
| 70 | + |
| 71 | +You can add custom \commands by supplying key=value pairs to the math syntax attribute: |
| 72 | + |
| 73 | + #![doc(math_syntax( |
| 74 | + // usage: $\floor{x}$ |
| 75 | + floor=r##"\delim{\lfloor}{#1}{\rfloor}"##, |
| 76 | + ))] |
| 77 | + |
| 78 | +## Reference-level explanation |
| 79 | +[reference-level-explanation]: #reference-level-explanation |
| 80 | + |
| 81 | +### Disabling and enabling math syntax |
| 82 | + |
| 83 | +The crate-level doc attributes `math_syntax` and `no_math_syntax` enable and disable |
| 84 | +support for parsing `$`-delimited TeX math in Rustdoc's Markdown. |
| 85 | + |
| 86 | +Obviously, you can't set both of them at the same time. If neither of them are set, |
| 87 | +rustdoc will use the edition-specific default (which is currently to disable it). |
| 88 | + |
| 89 | +The `math_syntax` attribute accepts an optional list of `key="value"` pairs for |
| 90 | +custom macros. This is similar to the `macros` parameter that KaTeX accepts, |
| 91 | +but the `key` part only includes the name of the macro, without the backslash |
| 92 | +or the number of parameters. The `"value"` is a string literal with TeX-like code, |
| 93 | +and, optionally, `#`numbered parameter placeholders. |
| 94 | + |
| 95 | +### Writing math code in markdown |
| 96 | + |
| 97 | +Math expressions are wrapped in `$` signs. One dollar sign means "inline" math, |
| 98 | +and two means "display" math. |
| 99 | + |
| 100 | +Inline math cannot have any whitespace at the start or end of its contents, |
| 101 | +so `$1$` is a math span, but `$ 1 $` is not. Inline math spans also |
| 102 | +can't be empty. |
| 103 | + |
| 104 | +Display math is allowed to have space at the start, |
| 105 | +so `$$ 1 $$` is a display math span. |
| 106 | + |
| 107 | +Unescaped curly braces within math spans must balance, |
| 108 | +and unescaped dollar signs can only appear between unescaped curly braces, |
| 109 | +so `$$ 1 {$} 2 $$` is parsed as a display math span, |
| 110 | +but `$$ 1 $ 2 $$` and `$$ { $$` are not. |
| 111 | + |
| 112 | +### Math syntax |
| 113 | + |
| 114 | +Within a math span, whitespaces are used for grouping and formatting. |
| 115 | +But you can't have more than one line break in a row within a math span, |
| 116 | +because that ends the paragraph that contains it. |
| 117 | + |
| 118 | +Other characters are usually rendered literally, except for |
| 119 | + |
| 120 | +- backslashes, `\`, which are the sigil for commands |
| 121 | +- curly braces, `{` and `}`, which are used for command arguments |
| 122 | +- dollar signs, `$`, which delimit math spans |
| 123 | +- number signs, `#`, which are used to refer to macro parameters |
| 124 | +- ampersands, `&`, which are used for writing matrices and tables |
| 125 | +- circumflex, `^`, which is used for exponents |
| 126 | +- underscore, `_`, which is used for subscript |
| 127 | +- single quote, `'`, which becomes the prime symbol |
| 128 | +- tile, `~`, which becomes a rendered, non-breaking space (since ordinary spaces are used for grouping) |
| 129 | +- percent, `%`, which mark line comments |
| 130 | +- NUL, which is not allowed |
| 131 | + |
| 132 | +Commands are used to write things that can't easily be typed on a keyboard, |
| 133 | +and for complex layouts like fractions and matrices. The math-core parser |
| 134 | +that we use implements hundreds of commands. |
| 135 | + |
| 136 | +<I>TODO: Full list is in <https://github.com/tmke8/math-core/blob/main/crates/math-core/src/commands.rs>. |
| 137 | +Do I need to include it all here?</I> |
| 138 | + |
| 139 | +## Drawbacks |
| 140 | +[drawbacks]: #drawbacks |
| 141 | + |
| 142 | +### There is no such thing as invalid Markdown |
| 143 | + |
| 144 | +Adding new syntax to Rustdoc's Markdown is rough, |
| 145 | +because it's so difficult to do without causing widespread breakage. |
| 146 | +As spelled out in the [CommonMark spec][], |
| 147 | +"any sequence of characters is a valid CommonMark document," |
| 148 | +so changing anything so that it acts like a metacharacter where it didn't used to |
| 149 | +changes the behavior of already-valid documents; |
| 150 | +a *breaking change.* |
| 151 | + |
| 152 | +And, unlike when GitHub redesigned their Markdown as a CommonMark dialect, |
| 153 | +we can't run a [one-time batch converter job][] over old crates.io crates [^ghmath]. |
| 154 | + |
| 155 | +This class of problem has come up when [intra-doc links were designed][], |
| 156 | +when [pulldown-cmark was last updated][], |
| 157 | +when [hoedown was replaced with pulldown-cmark in the first place][], |
| 158 | +and when [anyone proposes replacing Markdown with something else][] |
| 159 | +that has a "principled extension" system. |
| 160 | + |
| 161 | +[^ghmath]: |
| 162 | + Did GitHub run a similar batch job when they added math syntax? |
| 163 | + I can't think of any reason why they wouldn't, but I also can't find any proof that they did. |
| 164 | + It seems like it would require running the math-enabled parser over all the issue comments, |
| 165 | + and, if it detects math, add a backslash in front of the dollar signs. |
| 166 | + After all, math syntax didn't exist in GitHub Issues until they added it, |
| 167 | + so any detected math span is, by definition, a false positive. |
| 168 | + |
| 169 | +[CommonMark spec]: https://spec.commonmark.org/0.31.2/#characters-and-lines |
| 170 | +[one-time batch converter job]: https://github.blog/engineering/a-formal-spec-for-github-markdown/#the-migration |
| 171 | +[intra-doc links were designed]: https://github.com/rust-lang/rust/issues/54191 |
| 172 | +[pulldown-cmark was last updated]: https://github.com/rust-lang/rust/pull/121659#issuecomment-1992752820 |
| 173 | +[hoedown was replaced with pulldown-cmark in the first place]: https://internals.rust-lang.org/t/what-to-do-about-pulldown-and-commonmark/5115 |
| 174 | +[anyone proposes replacing Markdown with something else]: https://internals.rust-lang.org/t/rustdoc-restructuredtext-vs-markdown/356 |
| 175 | + |
| 176 | +### Verbosity or breakage as side effect |
| 177 | + |
| 178 | +From the perspective of 99% of doc authors who didn't want to write a math span in the first place, |
| 179 | +false positives that mangle their generated docs are a nasty papercut. |
| 180 | +Failing to escape the dollar signs when you needed to is not as bad as [accidentally triggering a link refdef][], |
| 181 | +since the degraded result might still be [legible][example rendering of a mistake], |
| 182 | +but having to read English text without any spaces sucks. |
| 183 | +Also, the LaTeX math syntax is forgiving enough that normal text is often valid, |
| 184 | +so Rustdoc compiler warnings won't catch every accidental match. |
| 185 | + |
| 186 | +But if we assume that every doc author adds the escapes that they need, |
| 187 | +this forces doc comments to have more escaped metacharacters than they used to. |
| 188 | +This makes doc comments less easily readable in their source form, |
| 189 | +imposing a cost on the 99% that don't want the feature in favor of the 1% who do. |
| 190 | + |
| 191 | +This argument, if taken to its logical extreme, would imply that we should use plain text |
| 192 | +doc comments with no extra formatting features. The downside of doing that is |
| 193 | +similar to the downside of not offering TeX math: users who *really* want bold text deploy |
| 194 | +[unicode crimes][] and pictures of text, which create accessibility problems. |
| 195 | + |
| 196 | +[accidentally triggering a link refdef]: https://github.com/rust-lang/rust/issues/133150 |
| 197 | +[example rendering of a mistake]: https://tmke8.github.io/math-core/#input:H4sIAAAAAAAAEwXBwQ0AIQgEwFa2guvCQvaBSoJIhETLv5n2uMIEu6P5MM2JODsF92iVONRBGEseFmsig_79a-7ZDjYAAAA= |
| 198 | +[unicode crimes]: https://ux.stackexchange.com/questions/118149/can-screen-readers-interpret-unicode-styles-fonts-such-as-bold-and-italics |
| 199 | + |
| 200 | +## Rationale and alternatives |
| 201 | +[rationale-and-alternatives]: #rationale-and-alternatives |
| 202 | + |
| 203 | +### Why TeX math in markdown, specifically? |
| 204 | + |
| 205 | +I would like to avoid the annoying scenario where Rustdoc deploys a complicated, special purpose language, then the community moves on to some new, incompatible language, and we’re stuck maintaining it ourselves because of the stability promise. |
| 206 | + |
| 207 | +There are a lot of special-purpose technical notations that we might theoretically want to support, |
| 208 | +but TeX-math-in-markdown is special, for two reasons: |
| 209 | + |
| 210 | +- [Lindy effect][]: LaTeX is an established standard that is not going anywhere any time soon. |
| 211 | +- There is more than one implementation of the subset of LaTeX that we need. |
| 212 | + |
| 213 | +The pull request I've been working on uses [math-core][], but, if that implementation turns out |
| 214 | +to be problematic, we could pivot to another one, like [pulldown-latex][], |
| 215 | +or [katex run in quick-js][] [^1]. |
| 216 | +That’s not an option with, for example, Typst. |
| 217 | + |
| 218 | +[Lindy effect]: https://en.wikipedia.org/wiki/Lindy_effect |
| 219 | +[math-core]: https://github.com/tmke8/math-core |
| 220 | +[pulldown-latex]: https://github.com/carloskiki/pulldown-latex |
| 221 | +[katex run in quick-js]: https://docs.rs/katex/latest/katex/ |
| 222 | + |
| 223 | +[^1]: |
| 224 | + I would prefer not to do *that*, because it's slow and seems to have poor error reporting, |
| 225 | + but, if we can't achieve good-enough feature support any other way, it's an option. |
| 226 | + |
| 227 | +## Prior art |
| 228 | +[prior-art]: #prior-art |
| 229 | + |
| 230 | +- <https://github.com/cben/mathdown/wiki/math-in-markdown> |
| 231 | +- <https://en.wikibooks.org/wiki/LaTeX/Mathematics> |
| 232 | +- The span parsing is based on the [math spec for commonmark-hs][], |
| 233 | + which is the parser used if you run `pandoc` in `gfm` mode. |
| 234 | +- Span parsing is documented in more detail in the [math spec for pulldown-cmark][]. |
| 235 | + |
| 236 | +[math spec for commonmark-hs]: https://github.com/jgm/commonmark-hs/blob/master/commonmark-extensions/test/math.md |
| 237 | +[math spec for pulldown-cmark]: https://pulldown-cmark.github.io/pulldown-cmark/specs/math.html |
| 238 | + |
| 239 | +## Unresolved questions |
| 240 | +[unresolved-questions]: #unresolved-questions |
| 241 | + |
| 242 | +### Avoiding Hyrum's Law |
| 243 | + |
| 244 | +There are a lot of \commands in [math-core][], and some of them are known buggy, |
| 245 | +meaning they don't match LaTeX itself. |
| 246 | +We don't want authors to rely on those bugs, either accidentally |
| 247 | +or in a workaround. |
| 248 | + |
| 249 | +Normally, we might "phase in" new commands by making them unstable first, |
| 250 | +letting more risk-tolerant authors try it out, |
| 251 | +then make it available to everyone else. |
| 252 | +But math-core doesn't have an API for that. |
| 253 | + |
| 254 | +### Font |
| 255 | + |
| 256 | +Right now, math formulas default to Noto Sans Math. |
| 257 | + |
| 258 | +This was chosen because it's inoffensive and fine. But it is a sans serif font face, that will usually be surrounded by serif people, |
| 259 | + |
| 260 | +Any suggestions? |
| 261 | + |
| 262 | + |
| 263 | +## Future possibilities |
| 264 | +[future-possibilities]: #future-possibilities |
| 265 | + |
| 266 | +### Undelimited environments |
| 267 | + |
| 268 | +It's a relatively rare feature, but Jupyter Notebook and a few others support |
| 269 | +LaTeX environments introduced with the `\begin{foo}` / `\end{foo}` syntax |
| 270 | +without wrapping dollar signs. |
| 271 | +Since backslashes in Markdown only have meaning when followed by punctuation, |
| 272 | +the false positives shouldn't be that common. |
| 273 | +Since we don't have to worry about false positives, |
| 274 | +we can treat it like a CommonMark block construct and allow blank lines in it. |
| 275 | + |
| 276 | + /// Computes sum from `start` to `end` of the given function. |
| 277 | + /// |
| 278 | + /// \begin{equation} |
| 279 | + /// \sum_{i=start}^{end}{f(i)} |
| 280 | + /// \end{equation} |
| 281 | + fn sum(f: impl FnMut(usize) -> usize, start: usize, end: usize) -> usize { |
| 282 | + let mut result = 0; |
| 283 | + for i in start..=end { |
| 284 | + result += f(i); |
| 285 | + } |
| 286 | + result |
| 287 | + } |
| 288 | + |
| 289 | +### Drawing and charting syntax |
| 290 | + |
| 291 | +There are a lot of different chart formats we *could* try to support. |
| 292 | +The tough part is that we want to support it long-term, |
| 293 | +give error messages at compile time (if the language has a concept of errors), |
| 294 | +and, ideally, have a specification without much churn. |
| 295 | + |
| 296 | +- [PlantUML][] is pretty much exactly what we would want. |
| 297 | + But we don't want to bundle a JRE. |
| 298 | +- The other obvious choice is [Mermaid][], because GitHub supports it. |
| 299 | + The upside is that it's popular and terse. The downside is that the only existing |
| 300 | + implementation is a JavaScript library. We could copy in the JS library and embed |
| 301 | + the source code into our HTML, but we wouldn't be able to give syntax errors at |
| 302 | + Rustdoc compile time that way. |
| 303 | +- If we supported undelimited LaTeX environment blocks, then it would make sense to |
| 304 | + implement a subset of the [LaTeX drawing tools][] on top of SVG. |
| 305 | + The downside is that the only other implementation of these languages that I know of is [LaTeXML][], which is not written in Rust. |
| 306 | + The upside is that integrating with the math engine lets you |
| 307 | + directly include equations and `math_syntax` macros in your graphics. |
| 308 | +- [Svgbob][] actually has a Rust implementation. Ironic, since Svgbob has no syntax errors, |
| 309 | + it's actually less important to have a Rust implementation than it is for the others, |
| 310 | + which have the possibility of an "invalid document" with errors that we would want |
| 311 | + to report at compile time. |
| 312 | + |
| 313 | +[PlantUML]: https://github.com/plantuml/plantuml |
| 314 | +[LaTeX drawing tools]: https://en.wikibooks.org/wiki/LaTeX/Introducing_Procedural_Graphics |
| 315 | +[LaTeXML]: https://en.wikipedia.org/wiki/LaTeXML |
| 316 | +[Mermaid]: https://github.com/mermaid-js/mermaid |
| 317 | +[Svgbob]: https://github.com/ivanceras/svgbob |
0 commit comments