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
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>");
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>");
}
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>");
}
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()),
}
}