diff --git a/src/docs/mathml.rs b/src/docs/mathml.rs index f9a0f60..9bb71de 100644 --- a/src/docs/mathml.rs +++ b/src/docs/mathml.rs @@ -15,6 +15,19 @@ //! Two forms are recognised, following the convention the documents use: //! `$...$` for a formula within a sentence and `$$...$$` for one on its own //! line. +//! +//! 为文档渲染数学记法。 +//! +//! 文档带有公式,读者需要它们看起来像公式而不是像源码。完整的 TeX 引擎是一个 +//! 庞大的依赖并自带字体文件,因此这里把记法翻译为 MathML。浏览器自行排版与打印 +//! MathML,因此无需运行任何脚本,输出在没有 JavaScript 时也可用。 +//! +//! 所覆盖的子集正是轨道力学实际使用的那些:分式、上标、下标、根式、希腊字母、 +//! 常见运算符,以及用于分组它们的定界符。子集之外的表达式按原样显示而不是丢弃, +//! 因此一个不被支持的命令不会有任何损失,只是显得朴素。 +//! +//! 识别两种形式,与文档所用的约定一致:句内公式用 `$...$`,独占一行用 +//! `$$...$$`。 use crate::docs::markdown::{escape, escape_attr}; @@ -22,9 +35,15 @@ use crate::docs::markdown::{escape, escape_attr}; /// /// `display` is set when the formula stands on its own line, which makes it /// larger and centred. +/// +/// 把公式转换为包在容器中的 MathML。 +/// +/// 当公式独占一行时置上 `display`,这使它更大并居中。 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" }; @@ -35,6 +54,11 @@ pub fn render(latex: &str, display: bool) -> String { // 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. + + // 原始源码保存在 data 属性中,使公式可以从页面恢复,供复制与测试使用。 + // MathML 是一个 token 一个元素,而浏览器复制选区时会逐个走过它们:从页面 + // 复制一条公式会得到每行一个字符。因此源码被携带在其旁一个读者可选的元素里, + // 而 MathML 被移出选区,使复制得到的是公式书写时的样子。 format!( "{body}{source}", source = escape_attr(latex), @@ -46,6 +70,10 @@ pub fn render(latex: &str, display: bool) -> String { /// /// Braces are turned into single characters so that a group is one unit, and /// the spacing commands are dropped. +/// +/// 把源码改写为翻译循环所读的记号。 +/// +/// 花括号被变成单字符,使一个组成为一个单位;空格类命令被丢弃。 pub(crate) fn normalize(latex: &str) -> Vec { let characters: Vec = latex.chars().collect(); let mut tokens: Vec = Vec::with_capacity(characters.len()); @@ -60,6 +88,8 @@ pub(crate) fn normalize(latex: &str) -> Vec { } '\\' => { // A command runs to the first non letter. + + // 命令一直延伸到第一个非字母字符。 let mut end = index + 1; while end < characters.len() && characters[end].is_ascii_alphabetic() { end += 1; @@ -68,6 +98,8 @@ pub(crate) fn normalize(latex: &str) -> Vec { // A backslash before a mark is a command of its own when // the mark is a spacing one, and that character itself // otherwise. + + // 反斜杠后若是空格类记号,它自成一个命令;否则那个字符本身。 match characters.get(index + 1) { Some(',') => tokens.push(Token::Command("thinspace".to_string())), Some(';') => tokens.push(Token::Command("thickspace".to_string())), @@ -94,50 +126,104 @@ pub(crate) fn normalize(latex: &str) -> Vec { } /// One unit of the formula after normalization. +/// +/// 归一化之后公式中的一个单位。 #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum Token { /// A single character. + + /// + /// 单个字符。 Symbol(char), /// A backslash command, without the backslash. + + /// + /// 一个反斜杠命令,不含反斜杠。 Command(String), /// An opening or closing brace; which one is taken from the position. + + /// + /// 一个开括号或闭括号;具体是哪一个由位置决定。 Group, } /// A parsed expression, ready to become MathML. +/// +/// 一个已解析的表达式,可以变为 MathML。 #[derive(Debug, Clone, PartialEq, Eq)] enum Node { /// Text, already escaped. + + /// + /// 文本,已转义。 Text(String), /// A row of children. + + /// + /// 一行子节点。 Row(Vec), /// A fraction. + + /// + /// 一个分式。 Fraction(Box, Box), /// A root. + + /// + /// 一个根式。 Root(Box, Option>), /// A superscript. + + /// + /// 一个上标。 Superscript(Box, Box), /// A subscript. + + /// + /// 一个下标。 Subscript(Box, Box), /// A subscript and a superscript on one base. + + /// + /// 同一个基上的下标与上标。 Scripts(Box, Box, Box), /// A named function such as `sin`. + + /// + /// 一个具名函数,例如 `sin`。 Function(String), /// A mark written above its argument, as in `\dot\Omega`. + + /// + /// 写在参数上方的一个符号,例如 `\dot\Omega`。 Overscript(Box, String), /// Words that stand as they are written, as in `\text{km}`. + + /// + /// 按书写原样保留的词,例如 `\text{km}`。 Verbatim(String), /// A matrix, with the delimiters its environment asks for. + + /// + /// 一个矩阵,带其环境所要求的定界符。 Matrix { /// The opening and closing delimiters, which may be empty. + + /// + /// 开定界符与闭定界符,可以为空。 open: &'static str, close: &'static str, /// The cells, one row at a time. + + /// + /// 各单元格,一次一行。 rows: Vec>, }, } /// The Greek letters, by command. +/// +/// 各希腊字母,按命令查。 fn greek(name: &str) -> Option<&'static str> { Some(match name { "alpha" => "α", @@ -185,6 +271,8 @@ fn greek(name: &str) -> Option<&'static str> { } /// The operators, by command. +/// +/// 各运算符,按命令查。 pub(crate) fn operator(name: &str) -> Option<&'static str> { Some(match name { "times" => "×", @@ -224,6 +312,8 @@ pub(crate) fn operator(name: &str) -> Option<&'static str> { } /// The functions, by command; these are set upright rather than italic. +/// +/// 各函数,按命令查;它们被排成直立体而不是斜体。 fn function(name: &str) -> Option<&'static str> { Some(match name { "sin" => "sin", @@ -253,6 +343,8 @@ fn function(name: &str) -> Option<&'static str> { } /// Renders a token sequence to MathML. +/// +/// 把一串记号渲染为 MathML。 pub(crate) fn to_mathml(tokens: &[Token]) -> String { let mut cursor = 0; let row = parse_row(tokens, &mut cursor, None); @@ -264,6 +356,8 @@ pub(crate) fn to_mathml(tokens: &[Token]) -> String { /// Parses until the end of the input or, when `stop` is given, until the /// group that ends the enclosing group. +/// +/// 一直解析到输入结束,或在给出 `stop` 时解析到结束外层组的那个记号。 fn parse_row(tokens: &[Token], cursor: &mut usize, stop: Option<&[char]>) -> Vec { let mut row: Vec = Vec::new(); @@ -271,6 +365,8 @@ fn parse_row(tokens: &[Token], cursor: &mut usize, stop: Option<&[char]>) -> Vec match &tokens[*cursor] { // A closing brace ends the enclosing group; a delimiter set // ends the stretchy group. + + // 闭括号结束外层组;一个定界符集合结束可变高的组。 Token::Group => { if stop.is_some_and(|set| set.contains(&'}')) { *cursor += 1; @@ -305,6 +401,8 @@ fn parse_row(tokens: &[Token], cursor: &mut usize, stop: Option<&[char]>) -> Vec } /// Returns the delimiters a matrix environment is drawn with. +/// +/// 返回某个矩阵环境绘制所用的定界符。 fn matrix_delimiters(name: &str) -> Option<(&'static str, &'static str)> { Some(match name { "matrix" | "aligned" | "align" | "gathered" | "array" => ("", ""), @@ -318,6 +416,8 @@ fn matrix_delimiters(name: &str) -> Option<(&'static str, &'static str)> { } /// Reads the name of an environment from the group that follows `\begin`. +/// +/// 从 `\begin` 之后的组中读出环境名。 fn parse_environment_name(tokens: &[Token], cursor: &mut usize) -> String { let mut name = String::new(); if !matches!(tokens.get(*cursor), Some(Token::Group)) { @@ -344,11 +444,21 @@ fn parse_environment_name(tokens: &[Token], cursor: &mut usize) -> String { /// A cell ends at `&`, a row ends at `\\`, and the environment ends at /// `\end`. The tokens of a cell are read by the same handlers as anywhere /// else, so a fraction or a root inside a cell works as it does outside. +/// +/// 读取矩阵的各行,直到闭合它的 `\end`。 +/// +/// 单元格在 `&` 处结束,一行在 `\\` 处结束,环境在 `\end` 处结束。单元格的 +/// 记号由与其他地方相同的处理函数读取,因此单元格内的分式或根式与在外面的表现 +/// 一致。 fn parse_matrix_rows(tokens: &[Token], cursor: &mut usize) -> Vec> { // Two accumulators: the cell being read, and the cells of the row so // far. One alone does not do, because `&` ends a cell and starts the // next, and the nodes after it belong to the next cell rather than // beside the one just closed. + + // 两个累加器:正在读取的单元格,以及本行此前的各单元格。只用一个不够, + // 因为 `&` 结束一个单元格并开启下一个,其后的节点属于下一个单元格,而不是 + // 与刚关闭的那个并列。 let mut rows: Vec> = Vec::new(); let mut cells: Vec = Vec::new(); let mut cell: Vec = Vec::new(); @@ -388,6 +498,8 @@ fn parse_matrix_rows(tokens: &[Token], cursor: &mut usize) -> Vec> { } } // The last row has no `\\` after it. + + // 最后一行之后没有 `\\`。 if !cell.is_empty() || !cells.is_empty() || rows.is_empty() { end_cell(&mut cells, &mut cell); rows.push(cells); @@ -399,6 +511,10 @@ fn parse_matrix_rows(tokens: &[Token], cursor: &mut usize) -> Vec> { /// /// Used where the argument is words rather than mathematics, so that the /// letters are not read as variables one at a time. +/// +/// 把参数的字符按其书写原样读取为文本。 +/// +/// 用于参数是词而不是数学的地方,使这些字母不会逐字被读成变量。 fn parse_raw_text(tokens: &[Token], cursor: &mut usize) -> String { let mut out = String::new(); if !matches!(tokens.get(*cursor), Some(Token::Group)) { @@ -420,6 +536,8 @@ fn parse_raw_text(tokens: &[Token], cursor: &mut usize) -> String { } /// Reads the argument of a command, which is either a group or one symbol. +/// +/// 读取命令的参数,它或是一个组,或是一个符号。 fn parse_argument(tokens: &[Token], cursor: &mut usize) -> Node { match tokens.get(*cursor) { Some(Token::Group) => { @@ -428,6 +546,8 @@ fn parse_argument(tokens: &[Token], cursor: &mut usize) -> Node { } // A square bracket is a group too, and is how a root takes its // index: `\sqrt[3]{x}`. + + // 方括号也是一个组,根式正是用它取指数:`\sqrt[3]{x}`。 Some(Token::Symbol('[')) => { *cursor += 1; let inner = parse_row(tokens, cursor, Some(&[',', ']'])); @@ -457,6 +577,8 @@ fn parse_argument(tokens: &[Token], cursor: &mut usize) -> Node { } /// Reads the scripts that follow a base, and returns the node with them. +/// +/// 读取跟随某个基的上下标,并返回带上它们的节点。 fn attach_scripts(base: Node, tokens: &[Token], cursor: &mut usize) -> Node { let mut superscript: Option = None; let mut subscript: Option = None; @@ -487,6 +609,8 @@ fn attach_scripts(base: Node, tokens: &[Token], cursor: &mut usize) -> Node { } /// The digit at a position. +/// +/// 某个位置上的数字。 fn characters_digit(tokens: &[Token], cursor: usize) -> char { match tokens.get(cursor) { Some(Token::Symbol(symbol)) => *symbol, @@ -495,11 +619,16 @@ fn characters_digit(tokens: &[Token], cursor: usize) -> char { } /// Handles a plain character, including the brackets that group. +/// +/// 处理一个普通字符,包括用于分组的括号。 fn parse_symbol(symbol: char, tokens: &[Token], cursor: &mut usize, _depth: &mut u32) -> Vec { let base = match symbol { // Digits are read together as one number, so that `12` is a number // rather than two identifiers. A dot between digits belongs to it, // which also stops the layout breaking the number across a line. + + // 数字被一起读成一个数,使 `12` 是一个数而不是两个标识符。数字之间的 + // 点属于它,这也阻止排版把这个数断成两行。 '0'..='9' => { let mut text = symbol.to_string(); loop { @@ -526,6 +655,12 @@ fn parse_symbol(symbol: char, tokens: &[Token], cursor: &mut usize, _depth: &mut // the group from here instead left `(a + b)^2` with the caret set as // an ordinary symbol and the exponent beside it, and inside a // fraction the exponent escaped the fraction altogether. + + // 开括号会伸展到其内容的高度。 + // + // 括号组是一个与其他基无异的基,因此由本函数末尾的同一处调用给它挂接 + // 上下标。若从这里直接返回该组,`(a + b)^2` 会把脱字符当作普通符号排版、 + // 指数落在其旁,而在分式内指数会整个逃出分式。 '(' => { let inner = parse_row(tokens, cursor, Some(&[',', ')'])); if matches!(tokens.get(*cursor), Some(Token::Symbol(')'))) { @@ -552,12 +687,17 @@ fn parse_symbol(symbol: char, tokens: &[Token], cursor: &mut usize, _depth: &mut '_' | '^' => Node::Text(symbol.to_string()), // The spaces are kept between words and dropped elsewhere, which is // what keeps `a b` apart without leaving gaps after a bracket. + + // 空格在词与词之间被保留、在其他地方被丢弃,这使 `a b` 分开,而括号之后 + // 不留空隙。 other => Node::Text(other.to_string()), }; vec![attach_scripts(base, tokens, cursor)] } /// Handles a backslash command. +/// +/// 处理一个反斜杠命令。 fn parse_command(name: &str, tokens: &[Token], cursor: &mut usize) -> Vec { if let Some(letter) = greek(name) { return vec![attach_scripts( @@ -576,6 +716,8 @@ fn parse_command(name: &str, tokens: &[Token], cursor: &mut usize) -> Vec if let Some(label) = function(name) { // A function is upright, so it is emitted as its own node rather // than as an italic variable. + + // 函数是直立体,因此它作为自己的节点输出,而不是作为一个斜体变量。 return vec![attach_scripts( Node::Function(label.to_string()), tokens, @@ -594,6 +736,9 @@ fn parse_command(name: &str, tokens: &[Token], cursor: &mut usize) -> Vec } // A matrix environment: \begin{name} cells \end{name}, with `&` // between the cells of a row and `\\` between the rows. + + // 矩阵环境:\begin{name} 各单元格 \end{name},行内单元格之间用 `&`, + // 各行之间用 `\\`。 "begin" => { let name = parse_environment_name(tokens, cursor); let Some((open, close)) = matrix_delimiters(&name) else { @@ -603,6 +748,9 @@ fn parse_command(name: &str, tokens: &[Token], cursor: &mut usize) -> Vec // The environment is closed by `\end{name}`, which is two // tokens: the command and the name. Both are read here, or the // name is left to be set as mathematics of its own. + + // 环境由 `\end{name}` 闭合,那是两个记号:命令与名字。两者都在此 + // 读取,否则名字会被留下、被当作数学单独排版。 if matches!(tokens.get(*cursor), Some(Token::Command(name)) if name == "end") { *cursor += 1; let _ = parse_raw_text(tokens, cursor); @@ -610,8 +758,12 @@ fn parse_command(name: &str, tokens: &[Token], cursor: &mut usize) -> Vec vec![Node::Matrix { open, close, rows }] } // The closing of an environment is consumed by the environment. + + // 环境的闭合由环境本身消费。 "end" => Vec::new(), // A floor, a ceiling, and the modulo operator. + + // 下取整、上取整,以及取模运算符。 "lfloor" | "lceil" | "langle" => { let symbol = match name { "lfloor" => "⌊", @@ -631,6 +783,9 @@ fn parse_command(name: &str, tokens: &[Token], cursor: &mut usize) -> Vec // A vertical bar names an absolute value or a norm. Written as a // command it is a delimiter like any other, and was being shown as // the letters of its own name. + + // 竖线表示绝对值或范数。写成命令时它与其他定界符无异,而此前它被显示成 + // 自己的名字那些字母。 "lvert" | "rvert" | "vert" => { vec![attach_scripts(Node::Text("|".to_string()), tokens, cursor)] } @@ -650,6 +805,8 @@ fn parse_command(name: &str, tokens: &[Token], cursor: &mut usize) -> Vec ])] } // An accent, written as a mark above its argument. + + // 一个重音符号,写在参数上方。 "dot" | "ddot" | "bar" | "overline" | "vec" | "hat" | "tilde" => { let mark = match name { "dot" => "˙", @@ -664,6 +821,8 @@ fn parse_command(name: &str, tokens: &[Token], cursor: &mut usize) -> Vec vec![attach_scripts(accented, tokens, cursor)] } // Commands that only set the size or the style, and carry no glyph. + + // 只设定大小或样式、不携带字形的命令。 "thinspace" => vec![Node::Text("\u{2009}".to_string())], "medspace" | "thickspace" => vec![Node::Text("\u{2005}".to_string())], "negspace" => Vec::new(), @@ -674,11 +833,15 @@ fn parse_command(name: &str, tokens: &[Token], cursor: &mut usize) -> Vec | "bigg" | "Bigg" => { // A size or a pairing command is structural. The delimiter that // follows is read as an ordinary symbol. + + // 大小命令或配对命令是结构性的。其后的定界符按普通符号读取。 Vec::new() } "sqrt" => { // The index, when there is one, is given in brackets before the // radicand: `\sqrt[3]{x}`. + + // 指数(若有)写在被开方式之前、方括号之中:`\sqrt[3]{x}`。 let degree = if matches!(tokens.get(*cursor), Some(Token::Symbol('['))) { Some(Box::new(parse_argument(tokens, cursor))) } else { @@ -693,9 +856,15 @@ fn parse_command(name: &str, tokens: &[Token], cursor: &mut usize) -> Vec } // A comma written as a command is punctuation, not a separator // between two formulas, so it keeps its own shape. + + // 写成命令的逗号是标点,而不是两条公式之间的分隔符,因此它保留自己的 + // 形状。 "," => vec![Node::Text(", ".to_string())], // `\text{...}` holds words as they are written. Read as variables // they would be set one letter at a time, so `km` became `k m`. + + // `\text{...}` 按书写原样保留词。若当作变量读取,它们会被逐字母排版, + // 于是 `km` 变成了 `k m`。 "text" | "mathrm" | "operatorname" | "mbox" => { let words = Node::Verbatim(parse_raw_text(tokens, cursor)); vec![attach_scripts(words, tokens, cursor)] @@ -703,12 +872,17 @@ fn parse_command(name: &str, tokens: &[Token], cursor: &mut usize) -> Vec // A style command keeps its argument set as mathematics. The group // it holds is a base, so it takes the scripts that follow it: the // `_central` of `\mathbf{a}_{\text{central}}` belongs to the `a`. + + // 样式命令让其参数仍按数学排版。它持有的组是一个基,因此接收其后的 + // 上下标:`\mathbf{a}_{\text{central}}` 中的 `_central` 属于那个 `a`。 "mathsf" | "mathtt" | "mathbf" | "mathit" | "mathcal" | "mathfrak" => { let inner = parse_argument(tokens, cursor); vec![attach_scripts(Node::Row(vec![inner]), tokens, cursor)] } // An unknown command is shown as written, so that a formula that // uses one still reads rather than losing part of itself. + + // 未知命令按原样显示,使使用了它的公式仍然可读,而不是丢掉自己的一部分。 other => vec![Node::Text(format!("\\{other}"))], } } @@ -720,6 +894,12 @@ fn parse_command(name: &str, tokens: &[Token], cursor: &mut usize) -> Vec /// a formula without any sign that something is missing: a multi-digit number /// such as the 4800 in `year + 4800` simply did not appear, and neither did /// the name `delta_v` before it was given an element. +/// +/// 把一段文本按其种类所要求的 MathML 元素写出。 +/// +/// 每个分支都产出一个元素,最后一个产出 ``。留在行内没有元素的文本会被 +/// 浏览器丢弃,这会丢掉公式的一部分而没有任何缺失的迹象:`year + 4800` 里的 +/// 4800 就完全没有出现,而在被赋予元素之前,名字 `delta_v` 也是如此。 fn classify(text: &str) -> String { if text.is_empty() { return String::new(); @@ -729,18 +909,26 @@ fn classify(text: &str) -> String { // A space is a space. It is written as an explicit one rather than as // text, because a run of spaces between two elements carries no glyph. + + // 空格就是空格。它被写成显式的空格而不是文本,因为两个元素之间的一串空格 + // 不携带任何字形。 if all(char::is_whitespace) { return "".to_string(); } // A variable is one letter, whatever script it is written in. Greek is // set as an identifier so that it is drawn as a variable rather than as // upright text. + + // 变量是一个字母,无论用哪种文字书写。希腊字母被排成标识符,使它被绘制为 + // 变量而不是直立体文本。 if text.chars().count() == 1 && first.is_alphabetic() { return format!("{}", escape(text)); } // A number starts with a digit. It may hold a decimal point or an // exponent, as in `1.5` or `1e-3`. + + // 数字以十进制数字开头。它可以含小数点或指数,例如 `1.5` 或 `1e-3`。 if first.is_ascii_digit() && all(|c| c.is_ascii_digit() || c == '.' || c == 'e' || c == 'E' || c == '-' || c == '+') { @@ -748,27 +936,41 @@ fn classify(text: &str) -> String { } // A name of Latin letters is an identifier, whether it is one letter or // several, as in `a` or `delta_v`. + + // 拉丁字母构成的名字是标识符,无论是一个字母还是多个,例如 `a` 或 + // `delta_v`。 if all(|c| c.is_ascii_alphabetic()) { return format!("{}", escape(text)); } // An operator, including one written with its spacing, as in ` = `. // The spaces are the operator's own width, so they are trimmed before // the test rather than counted against it. + + // 一个运算符,包括带空格书写的那些,例如 ` = `。空格是运算符自身的宽度, + // 因此在判断之前先去掉它们,而不是把它们算作否定条件。 let trimmed = text.trim(); if !trimmed.is_empty() && trimmed.chars().all(|c| !c.is_alphanumeric()) { return format!("{}", escape(trimmed)); } // Anything else is text: a label in another script, or a phrase. It is // set upright and kept whole rather than being read as variables. + + // 其余一切都是文本:另一种文字写成的标签,或一个短语。它被排成直立体并保持 + // 完整,而不是被读成变量。 format!("{}", escape(text)) } /// Writes a node as MathML. +/// +/// 把一个节点写为 MathML。 fn emit(node: &Node) -> String { match node { // MathML distinguishes an identifier, a number and operators, and a // browser uses the element to choose the font: an upright digit in a // serif face reads as a number rather than as a variable. + + // MathML 区分标识符、数字与运算符,浏览器用元素来选择字体:衬线体中的 + // 直立数字读起来是数字而不是变量。 Node::Text(text) => classify(text), Node::Function(label) => format!("{}", escape(label)), Node::Verbatim(text) => format!("{}", escape(text)), @@ -799,6 +1001,9 @@ fn emit(node: &Node) -> String { // A row of one is the child itself; an extra pair of brackets // would be noise in the markup and would break the grouping the // element's layout relies on. + + // 只含一个子节点的一行就是该子节点本身;多一对括号只会成为标记中的 + // 噪声,并破坏元素布局所依赖的分组。 1 => emit(&children[0]), _ => { let body: String = children.iter().map(emit).collect();