1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
//! Writes sample formulas to standard output as a single HTML page
//!
//! Usage:
//! cargo run > sample.html
//! cargo run -- 'x^2 + y^2 = r^2' '\frac{a}{b}' > out.html
//! cargo run -- --font /path/to/other-math.otf > out.html
use suki::font::{self, Font};
use suki::{Mode, Shape, escape, render_html, render_svg, style};
const DEFAULT_FONT: &str = "font/latinmodern-math.otf";
const SAMPLES: &[&str] = &[
r"x^2 + y^2 = r^2",
r"x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}",
r"e^{i\pi} + 1 = 0",
r"\sum_{k=1}^{n} k = \frac{n(n+1)}{2}",
r"\int_0^1 x^2 \, dx = \frac{1}{3}",
r"\lim_{x \to \infty} \left( 1 + \frac{1}{x} \right)^{x} = e",
r"\sin^2 \theta + \cos^2 \theta = 1",
r"a_{ij} \neq 0, \quad i \in \{1, \ldots, n\}",
r"\sqrt[3]{\frac{x + 1}{x - 1}}",
r"\binom{n}{k} = \frac{n!}{k!\,(n-k)!}",
r"\left\{ x \in \mathbb{R} \mid x > 0 \right\}",
r"A = \begin{pmatrix} a & b \\ c & d \end{pmatrix}",
r"\det A = \begin{vmatrix} a & b \\ c & d \end{vmatrix} = ad - bc",
r"I_n = \begin{bmatrix} 1 & \cdots & 0 \\ \vdots & \ddots & \vdots \\ 0 & \cdots & 1 \end{bmatrix}",
r"\begin{array}{lcr} \hline a & bb & ccc \\ \hline 1 & 22 & 333 \\ \hline \end{array}",
r"f(x) = \begin{cases} 1 & x > 0 \\ 0 & x \leq 0 \end{cases}",
r"\overline{z_1 z_2} = \overline{z_1}\, \overline{z_2}",
r"\hat{p} \quad \vec{v} \quad \bar{x} \quad \dot{q} \quad \ddot{q} \quad \tilde{n}",
r"\mathbf{A}\mathbf{x} = \lambda \mathbf{x}, \quad \mathrm{d}x, \quad \mathbb{Z} \subset \mathbb{R}",
r"P(A \mid B) = \frac{P(B \mid A)\, P(A)}{P(B)} \quad \text{ベイズの定理}",
r"\nabla \times \mathbf{B} = \mu_0 \left( \mathbf{J} + \varepsilon_0 \frac{\partial \mathbf{E}}{\partial t} \right)",
r"\left[ \frac{1}{\sqrt{\frac{a}{b} + \frac{c}{d}}} \right]",
r"\frac{a}{b",
];
const PAGE_STYLE: &str = "body{margin:0;padding:56px 40px 96px;color:#1f1f1f;background:#fff;\
font-family:system-ui,-apple-system,sans-serif;font-size:14px;line-height:1.9}
h1{font-size:19px;font-weight:500;margin:0 0 8px}
p{max-width:46em;margin:0 0 40px;color:#767676}
.item{margin-bottom:40px}
.source{color:#767676;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;\
line-height:1.6;margin-bottom:14px;word-break:break-all}
.pair{display:flex;flex-wrap:wrap;align-items:baseline;column-gap:48px;row-gap:16px}
.render{display:flex;align-items:baseline;gap:14px}
.label{color:#767676;font-size:11px;flex:none;width:74px}
.formula{font-size:21px;overflow-x:auto;max-width:100%}
.note{color:#767676;font-size:12px}";
fn main() {
let mut arguments: Vec<String> = std::env::args().skip(1).collect();
let mut path = DEFAULT_FONT.to_string();
if let Some(index) = arguments.iter().position(|value| value == "--font") {
if index + 1 >= arguments.len() {
eprintln!("--font must be followed by a font file");
std::process::exit(1);
}
path = arguments.remove(index + 1);
arguments.remove(index);
}
let data = match std::fs::read(&path) {
Ok(data) => data,
Err(reason) => {
eprintln!("cannot read the font: {path}: {reason}");
std::process::exit(1);
}
};
let font = match font::load(data) {
Ok(font) => font,
Err(reason) => {
eprintln!("cannot load the font: {path}: {reason}");
std::process::exit(1);
}
};
let formulas: Vec<&str> = if arguments.is_empty() {
SAMPLES.to_vec()
} else {
arguments.iter().map(String::as_str).collect()
};
write_page(&font, &path, &formulas);
}
fn write_page(font: &Font, path: &str, formulas: &[&str]) {
println!("<!DOCTYPE html>");
println!("<html lang=\"en\">");
println!("<head>");
println!("<meta charset=\"utf-8\">");
println!("<title>suki</title>");
println!("<style>");
// The font may not be installed where the sample is opened, so have the browser load it
// from the file. Formulas typeset as outlines show correctly without it
println!(
"@font-face{{font-family:\"{}\";src:url(\"{}\")}}",
escape(font::family(font)),
escape(path)
);
println!("{}{PAGE_STYLE}", style(font));
println!("</style>");
println!("</head>");
println!("<body>");
println!("<h1>suki</h1>");
println!(
"<p>The typeface is {}. Each formula is typeset in 3 ways. HTML lays out characters, \
SVG text places characters inside SVG, and SVG outline draws the glyphs as outlines. \
Outlines need no font where they are displayed. Placed in a sentence, it looks like this: {} \
and the sentence ends here.</p>",
escape(font::family(font)),
inline(font, r"e^{i\pi} + 1 = 0")
);
for formula in formulas {
println!("<div class=\"item\">");
println!("<div class=\"source\">{}</div>", escape(formula));
println!("<div class=\"pair\">");
write_render("HTML", &render_html(formula, font, Mode::Display));
write_render("SVG text", &render_svg(formula, font, Mode::Display, Shape::Text));
write_render("SVG outline", &render_svg(formula, font, Mode::Display, Shape::Outline));
println!("</div>");
println!("</div>");
}
println!("</body>");
println!("</html>");
}
/// Writes the result of one way of typesetting
fn write_render(label: &str, result: &Result<String, suki::Error>) {
let body = match result {
Ok(markup) => format!("<div class=\"formula\">{markup}</div>"),
Err(reason) => format!("<div class=\"note\">Error: {}</div>", escape(&reason.to_string())),
};
println!("<div class=\"render\"><div class=\"label\">{label}</div>{body}</div>");
}
/// Typesets in the style used within text
fn inline(font: &Font, source: &str) -> String {
match render_svg(source, font, Mode::Inline, Shape::Outline) {
Ok(markup) => markup,
Err(reason) => escape(&reason.to_string()),
}
}