Registry / RepositoryTypeScriptRustPython
Rollpie
ReadmeFiles
Versions
Info
Download
0.2.3555.5 KB2026-10-160.2.2572.8 KB2026-10-150.2.1580.5 KB2026-10-150.2.0579.9 KB2026-09-150.1.1576.6 KB2026-09-12
Version
0.2.2
Copyright
Rollpie, Irohabook
Publisher
math
Published
2026-10-15
Size
572.8 KB
Downloads
28
Checksum
ce26f898ba26eb49c71ebc68597f62f268b83e41bb265b1b5a01688966054588
Dependencies
None

check.rs

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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
//! Writes out an inspection page to check by eye in a browser
//!
//! Usage:
//!   cargo run --example check > check.html && open check.html
//!   cargo run --example check -- /path/to/other-math.otf > check.html
//!
//! Shows 10 typical formulas one below another, each in 3 kinds of output. The 3 should
//! come out in the same shape, so a place where they differ is an error. A faint line is
//! drawn on the baseline, so you can also see whether the vertical positions line up.

use suki::font::{self, Font};
use suki::{Mode, Shape, escape, render_html, render_svg, style};


const DEFAULT_FONT: &str = "font/latinmodern-math.otf";


/// (heading, formula, lead-in placed in a sentence, what to look at)
const CHECKS: &[(&str, &str, &str, &str)] = &[
    (
        "Superscripts and spacing",
        r"x^2 + y^2 = r^2",
        "The equation of a circle is",
        "The space on each side of = is wider than the space on each side of +. \
The 2 is at the upper right of x and does not go above the height of the characters.",
    ),
    (
        "Fractions and radicals",
        r"x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}",
        "The solutions of a quadratic equation are",
        "The fraction bar is at the same height as =. The horizontal line of the radical \
joins the top of the radical sign and covers all of the content. \
The − at the start is not a subtraction, so it has no extra space on either side.",
    ),
    (
        "Large operators and scripts above and below",
        r"\sum_{k=1}^{n} k = \frac{n(n+1)}{2}",
        "The sum from 1 to n is",
        "∑ has scripts directly above and below it, and both are centered. \
∑ itself is larger than the ordinary characters. In the copy placed in the sentence, \
the scripts move to the right and do not disturb the line height.",
    ),
    (
        "Integrals and thin spaces",
        r"\int_0^1 x^2 \, dx = \frac{1}{3}",
        "The definite integral is",
        "The scripts of ∫ are attached to its right, not above and below it. \
            There is a small space between x^2 and dx. The d in dx is italic, not upright.",
    ),
    (
        "Stretchy delimiters",
        r"\lim_{x \to \infty} \left( 1 + \frac{1}{x} \right)^{x} = e",
        "Euler's number is",
        "The parentheses grow taller to fit the fraction inside and extend far enough \
above and below it. The x is at the upper right of the parentheses. \
lim is upright, and its script is placed below it.",
    ),
    (
        "Matrices",
        r"A = \begin{pmatrix} a & b \\ c & d \end{pmatrix}",
        "A square matrix of order 2 is",
        "The parentheses stretch to the height of 2 rows. The columns line up vertically, \
and the rows are evenly spaced. The center of the whole matrix is at the height of =.",
    ),
    (
        "Cases",
        r"f(x) = \begin{cases} 1 & x > 0 \\ 0 & x \leq 0 \end{cases}",
        "The step function is",
        "The brace covers the 2 rows. The left and right columns are each left-aligned.",
    ),
    (
        "Radicals with an index, and nesting",
        r"\sqrt[3]{\frac{x + 1}{x - 1}}",
        "The cube root is",
        "The 3 is at the upper left of the radical and does not overlap the sign. \
The radical covers the whole nested fraction.",
    ),
    (
        "Accents and lines above",
        r"\hat{p} \quad \vec{v} \quad \overline{z_1 z_2} \quad \widehat{xyz}",
        "The estimator and the conjugate are",
        "Each accent is directly above its letter. The line over z_1 z_2 is a single line \
across the 2 letters. The accent of \\widehat{xyz} spans 3 letters and is wider than \
the one of \\hat{p}.",
    ),
    (
        "Styles and text",
        concat!(
            r"\mathbf{A}\mathbf{x} = \lambda \mathbf{x}, \quad ",
            r"\mathbb{Z} \subset \mathbb{R}, \quad \text{固有値}"
        ),
        "The eigenvalue equation is",
        "A and x are bold upright, and λ is italic. ℤ and ℝ are blackboard bold. \
The Japanese inside \\text is in the same typeface as the body text, \
and its characters line up without overlapping.",
    ),
];


const PAGE_STYLE: &str = "\
body{margin:0;padding:56px 44px 120px;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 10px}
h2{font-size:14px;font-weight:500;margin:0 0 10px}
.lead{max-width:48em;margin:0 0 12px;color:#767676}
.item{margin-bottom:52px;max-width:60em}
.source{color:#767676;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;\
line-height:1.7;margin-bottom:18px;word-break:break-all}
.row{position:relative;margin-bottom:14px;display:flex;align-items:baseline;gap:16px}
.name{color:#767676;font-size:11px;width:62px;flex:none;text-align:right}
.stage{position:relative;flex:1;min-width:0;overflow-x:auto}
.guide{display:inline-block;width:100%;height:0;margin-right:-100%;\
border-top:1px dashed #dcdcdc;vertical-align:baseline}
.formula{font-size:27px;position:relative}
.sentence{margin-top:20px;font-size:15px;max-width:46em}
.watch{margin-top:12px;color:#767676;font-size:13px;max-width:46em}
.note{color:#767676;font-size:13px}";


fn main() {
    let arguments: Vec<String> = std::env::args().skip(1).collect();
    let path = arguments.first().map_or(DEFAULT_FONT, |value| value.as_str());
    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);
        }
    };

    write_page(&font, path);
}


fn write_page(font: &Font, path: &str) {
    println!("<!DOCTYPE html>");
    println!("<html lang=\"en\">");
    println!("<head>");
    println!("<meta charset=\"utf-8\">");
    println!("<title>suki check</title>");
    println!("<style>");
    // The viewing machine may not have this typeface, so make the browser load it from the file.
    // Formulas typeset with outlines show correctly even without this
    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 check</h1>");
    println!(
        "<p class=\"lead\">The typeface is {}. Below are 10 typical formulas, \
each in 3 kinds of output.</p>",
        escape(font::family(font))
    );
    println!(
        "<p class=\"lead\">HTML is characters set in a row, SVG text is characters placed \
in an SVG, and SVG outline is glyphs drawn as outlines. The 3 should look the same, \
so if only 1 of them differs, that one is wrong.</p>"
    );
    println!(
        "<p class=\"lead\">Stretchy delimiters, radicals, and large operators in display mode \
are drawn as outlines in every kind of output. This is because the font has them as \
size variants, but no characters are assigned to those glyphs, \
so they cannot be written out as characters.</p>"
    );
    println!(
        "<p class=\"lead\">The faint dashed line is the baseline. \
If a formula sits on this line, it lines up with the characters around it. \
Below each formula are the same formula placed in body text, and what to look at.</p>"
    );

    for (index, (heading, source, lead, watch)) in CHECKS.iter().enumerate() {
        write_item(font, index + 1, heading, source, lead, watch);
    }

    println!("</body>");
    println!("</html>");
}


fn write_item(font: &Font, number: usize, heading: &str, source: &str, lead: &str, watch: &str) {
    println!("<div class=\"item\">");
    println!("<h2>{number}. {}</h2>", escape(heading));
    println!("<div class=\"source\">{}</div>", escape(source));
    write_row("HTML", &render_html(source, font, Mode::Display));
    write_row("SVG text", &render_svg(source, font, Mode::Display, Shape::Text));
    write_row("SVG outline", &render_svg(source, font, Mode::Display, Shape::Outline));

    let inline = match render_svg(source, font, Mode::Inline, Shape::Outline) {
        Ok(markup) => markup,
        Err(reason) => escape(&reason.to_string()),
    };
    println!(
        "<p class=\"sentence\">{} {inline}. Check that this line's height is not disturbed.</p>",
        escape(lead)
    );
    println!("<p class=\"watch\">What to look at: {}</p>", escape(watch));
    println!("</div>");
}


fn write_row(name: &str, result: &Result<String, suki::Error>) {
    let body = match result {
        Ok(markup) => format!("<span class=\"guide\"></span><span class=\"formula\">{markup}</span>"),
        Err(reason) => format!("<span class=\"note\">Error: {}</span>", escape(&reason.to_string())),
    };

    println!("<div class=\"row\"><div class=\"name\">{name}</div><div class=\"stage\">{body}</div></div>");
}