Write the comments in both languages, part eight

This commit is contained in:
dev 2026-10-02 13:37:46 +08:00
parent 8b37a187aa
commit 89f1b0bc9a
1 changed files with 205 additions and 0 deletions

View File

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