From ec78ac009b6ef08fe82234d63acd11a3096f4fb4 Mon Sep 17 00:00:00 2001 From: dev Date: Fri, 2 Oct 2026 12:06:11 +0800 Subject: [PATCH] Let a formula be copied as the formula Copying a formula from a page gave one character per line: the SGP4 a_1 block arrived as a hundred and thirty-two lines for a hundred and thirty-two characters. That is how a browser copies MathML, which is one element per token and is walked one element at a time. The rendering was not the fault. The block was measured in place and the six rows each hold their two cells side by side, with the tokens of a row sharing a baseline, and a screenshot agrees. What was wrong is what a reader gets when they take the formula away. The source is therefore carried beside the MathML in an element that can be selected, and the MathML is taken out of selection. A copy now yields the notation the formula was written in, on one line, which is what a reader can paste into a tool that understands it. The source is trimmed when it is rendered, because the delimiters of a display formula sit on their own lines and the newline after the opening pair was arriving as a leading space. --- frontend/src/app.css | 25 +++++++++++++++++++++++++ src/docs/mathml.rs | 13 ++++++++++--- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/frontend/src/app.css b/frontend/src/app.css index 8221411..f88fbce 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -1542,6 +1542,31 @@ tbody tr:hover { line-height: 1.5; } +/* + * The MathML is taken out of text selection and the source is offered in its + * place. + * + * A formula is one element per token, and a browser copying a selection walks + * those elements one at a time: copying `a_1 = (x_ke / n_0)^{2/3}` from the + * page gave one character per line, 132 lines for 132 characters. What a + * reader wants on the clipboard is the formula, so the source is what can be + * selected and the markup cannot. + */ +.math math { + user-select: none; +} + +/* Kept out of the layout but not out of a selection, so that a copy finds it + where the formula is. */ +.math-source { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; +} + .math-display { display: block; margin: 1.1rem 0; diff --git a/src/docs/mathml.rs b/src/docs/mathml.rs index 597a92b..2de2bce 100644 --- a/src/docs/mathml.rs +++ b/src/docs/mathml.rs @@ -23,14 +23,21 @@ use crate::docs::markdown::{escape, escape_attr}; /// `display` is set when the formula stands on its own line, which makes it /// larger and centred. pub fn render(latex: &str, display: bool) -> String { + // The delimiters of a display formula are on their own lines, so the + // source arrives with the newline that followed the opening pair. + let latex = latex.trim(); let body = to_mathml(&normalize(latex)); let class = if display { "math math-display" } else { "math" }; // The original source is kept in a data attribute so that the formula // can be recovered from the page, for copying and for the tests. + // The MathML is one element per token, and a browser copying a selection + // walks them one at a time: a formula copied from the page arrives as one + // character per line. The source is therefore carried beside it in an + // element a reader can select, and the MathML is taken out of selection + // so that what a copy yields is the formula as it was written. format!( - "{body}", - escape_attr(latex), + "{body}{source}", + source = escape_attr(latex), display = if display { "block" } else { "inline" } ) }