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
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
use lina::font::{self, Font};
use lina::{Shape, Style, escape, render_svg, render_svg_with};
const DEFAULT_FONT: &str = "font/latinmodern-math.otf";
const GROUPS: &[(&str, &[(&str, &str)])] = &[
("Bonds", &[
("H-O-H",
"The bonds stop at the edge of the ink of H and O without touching it. \
They are level and cross the middle of the letters"),
("O=C=O", "The two lines are the same length, equally far above and below the middle"),
("N~N", "Three evenly spaced lines; the middle one lies halfway between the outer two"),
("C-C", "The bond is as long as in H-O-H. The atom centers are 3em apart"),
("H_3C-CH_3",
"The bond stops at the last C on the left and the first C on the right. \
It is shorter than in C-C"),
]),
("Angles and branches", &[
("A-[:90]B", "B is straight above A"),
("A-[2]B", "[2] is 2 x 45 degrees, straight up. Same direction as above"),
("A-[:30]B-[::60]C",
"C is straight above B. The relative angle is added to the 30 degrees before it"),
("A-[::30]B-[::30]C-[::30]D-[::30]E",
"Repeating the same angle draws an arc with equal steps"),
("A-[,0.5]B-[,1]C-[,2]D", "Bond lengths of 0.5, 1 and 2 times. C-D is 4 times A-B"),
("H-[:30]C(-[:90]H)(-[:-90]H)-[:-30]H",
"Two branches. All four bonds leave from the edge of C"),
("-[:30]-[:-30]-[:30]-[:-30]", "Nodes with no label. The zigzag has no breaks or gaps"),
]),
("Rings", &[
("*6(------)",
"A regular hexagon. The top and bottom sides are level, with points at left and \
right. All six sides are equal"),
("*5(-----)", "A regular pentagon. All five sides are equal"),
("*3(---)", "An equilateral triangle"),
("*6(-=-=-=)",
"The second line of each double bond is inside the ring and stops short of the \
corners"),
("**6(------)", "The circle sits inside the hexagon and touches no side"),
("H_3C-*6(-=-=-=)",
"The ring extends straight along the bond. CH_3 joins the left vertex of the ring"),
("*6(-N=-=-=)",
"N is at the upper left vertex, and both neighboring sides stop at the edge of N"),
("*6(-(-OH)-----)",
"A branch with no angle points out of the ring. OH does not overlap the ring"),
("*6(--*5(-----)----)",
"The hexagon and the pentagon share one vertex. The pentagon points out of the \
hexagon, and on both sides of the shared vertex the angles between sides are equal"),
]),
("Ring closure", &[
("?[a]-[:0]-[:-90]-[:180]?[a]", "Three bonds and one from the marks close a square"),
("*6(--?[a]-(-[:0]-[:60]-[:120]-[:180]?[a])---)",
"Two hexagons share one side. The skeleton of decalin"),
("*6(-=?[a]-(=[:0]-[:60]=[:120]-[:180]?[a,=])=-=)",
"Naphthalene. Double bonds alternate in both rings"),
("A?[b,=]-[:60]B-[:-60]C?[b]",
"The bond the marks draw between A and C is double and forms the base of the triangle"),
]),
("Wedges", &[
("X(>[:0]a)(<[:60]b)(>:[:120]c)(<:[:180]d)(>|[:240]e)(<|[:300]f)",
"a and b are filled triangles, c and d are hashed, e and f are hollow triangles. \
a, c and e are narrow at X; b, d and f are wide at X"),
("H_3C-C(>[:90]OH)(<:[:-90]H)-CH_2-CH_3",
"The bond toward OH, in front, is a filled triangle; the bond toward H, behind, is \
hashed"),
]),
("LaTeX wrappers", &[
("\\chemfig{H_3C-CH_2-OH}",
"The wrapper is taken off and the inside is set. The figure is the same as without \
the wrapper"),
("\\chemname{\\chemfig{H-O-H}}{water}", "The name is centered under the figure"),
("\\chemfig{A-[45]B}",
"Even with a wrapper, the position of a mistake counts in the original string: \
character 13"),
]),
("Labels", &[
("H_2O", "The 2 is smaller and sits below the baseline"),
("H_{2_{2_{2}}}",
"Nested subscripts stop shrinking at the third. The second and third are the same \
size"),
("Na^+", "The + is small and raised, level at top and bottom with a superscript digit"),
("Ca^{2+}", "The 2 and the + are level. The + is drawn around the math axis, \
so it is raised to the height of the digits"),
("SO_4^{2-}", "2- is raised as a group, and the minus crosses the middle of the 2"),
("H_2SO_4",
"Both subscripts have the same height and size. There is a little space between the \
2 and the S"),
("H_2N-CH_2-C(=[:90]O)-OH", "Glycine. The three level bonds run at the same height"),
]),
("Error reports", &[
("A-[45]B", "Says in red what is wrong and at which character"),
("*6(-----)", "Says that a ring of size 6 has only 5 bonds"),
("A?[a]-B", "Says that there is only one mark"),
]),
];
const INLINE: &str = "H_3C-CH_2-OH";
const VARIED: &str = "*6(-=-(-OH)=-=)";
const SHAPED: &str = "H_2SO_4";
const PAGE_STYLE: &str = "body{margin:0;padding:56px 40px 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 8px}
h2{font-size:15px;font-weight:500;margin:64px 0 20px;color:#1f1f1f}
p{max-width:46em;margin:0 0 32px;color:#767676}
.item{display:grid;grid-template-columns:minmax(0,20em) minmax(0,22em);column-gap:32px;\
align-items:start;margin-bottom:36px}
.source{grid-column:1 / -1;color:#767676;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;\
font-size:12px;line-height:1.6;margin-bottom:10px;word-break:break-all}
.figure{font-size:16px;overflow-x:auto;max-width:100%}
.note{color:#767676;font-size:12px;line-height:1.8}
.wrong{color:#a33;font-size:12px}
.inline{max-width:46em;color:#1f1f1f;font-size:16px;line-height:2.4}
.row{display:flex;flex-wrap:wrap;align-items:flex-end;gap:40px}
.dark{background:#1f1f1f;color:#fff;padding:24px 28px;display:inline-block;font-size:16px}
.tint{color:#1b81e0;font-size:16px}
.tone{color:#a33;font-size:16px}";
fn main() {
let mut argument: Vec<String> = std::env::args().skip(1).collect();
let mut path = DEFAULT_FONT.to_string();
if let Some(index) = argument.iter().position(|value| value == "--font") {
if index + 1 >= argument.len() {
eprintln!("give a font file after --font");
std::process::exit(1);
}
path = argument.remove(index + 1);
argument.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);
}
};
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>Checking lina</title>");
println!("<style>");
println!(
"@font-face{{font-family:\"{}\";src:url(\"{}\")}}",
escape(font::family(font)),
escape(path)
);
println!("{PAGE_STYLE}</style>");
println!("</head>");
println!("<body>");
println!("<h1>Checking lina</h1>");
println!(
"<p>The typeface is {}. Next to each figure is what to look at to tell that it is \
right. If a figure does not look as described, that is the mistake. The last three in the \
list are given malformed input on purpose; the right result is the reason and the position \
in red.</p>",
escape(font::family(font))
);
for (title, items) in GROUPS {
println!("<h2>{}</h2>", escape(title));
for (source, note) in *items {
write_item(font, source, note);
}
}
write_shape(font);
write_inline(font);
write_varied(font);
println!("</body>");
println!("</html>");
}
fn write_shape(font: &Font) {
println!("<h2>Outlines and text</h2>");
println!(
"<p>The same molecule written out two ways. The left draws the glyphs as outlines, \
and the right places them as text. It is right if the two look the same and only the right \
one can be selected and copied. Copying it gives {}. The right one uses the font this page \
loads, so it changes shape where that font is missing.</p>",
escape(SHAPED)
);
println!("<div class=\"source\">{}</div>", escape(SHAPED));
println!("<div class=\"row\">");
println!("<div class=\"figure\">{}</div>", draw(font, SHAPED));
let style = Style::default();
match render_svg_with(SHAPED, font, &style, Shape::Text) {
Ok(markup) => println!("<div class=\"figure\">{markup}</div>"),
Err(reason) => println!("<div class=\"wrong\">{}</div>", escape(&reason.to_string())),
}
println!("</div>");
}
fn write_item(font: &Font, source: &str, note: &str) {
println!("<div class=\"item\">");
println!("<div class=\"source\">{}</div>", escape(source));
match render_svg(source, font) {
Ok(markup) => println!("<div class=\"figure\">{markup}</div>"),
Err(reason) => {
println!("<div class=\"wrong\">{}</div>", escape(&reason.to_string()))
}
}
println!("<div class=\"note\">{}</div>", escape(note));
println!("</div>");
}
fn write_inline(font: &Font) {
let figure = match render_svg(INLINE, font) {
Ok(markup) => markup,
Err(reason) => escape(&reason.to_string()),
};
println!("<h2>In running text</h2>");
println!(
"<p>Figures are written in the same unit as the text around them. In the line below, \
it is right if the bonds sit at the middle height of the letters around them and the line \
height is not stretched unnaturally.</p>"
);
println!("<div class=\"inline\">Ethanol {figure} also dissolves well in water.</div>");
}
fn write_varied(font: &Font) {
println!("<h2>Size and color</h2>");
println!(
"<p>The same molecule with only the font size and the color changed. It is right if the \
three figures keep their shape and change only in size, and the color follows the text. The \
last one sits on a dark background, and it is right if both lines and letters come out \
white.</p>"
);
println!("<div class=\"source\">{}</div>", escape(VARIED));
println!("<div class=\"row\">");
for size in [13, 18, 26] {
let figure = draw(font, VARIED);
println!("<div style=\"font-size:{size}px\">{figure}</div>");
}
println!("</div>");
println!("<div class=\"row\" style=\"margin-top:40px\">");
println!("<div class=\"tint\">{}</div>", draw(font, VARIED));
println!("<div class=\"tone\">{}</div>", draw(font, VARIED));
println!("<div class=\"dark\">{}</div>", draw(font, VARIED));
println!("</div>");
println!("<h2>Changed dimensions</h2>");
println!(
"<p>The bond length and the line width changed through Style. The left is the default, \
the middle has the bond length cut to 2em, and the right has thicker lines. It is right if the \
shape holds and only what was set changes.</p>"
);
println!("<div class=\"row\">");
println!("<div class=\"figure\">{}</div>", draw(font, VARIED));
let narrow = Style {bond_length: 2.0, ..Style::default()};
println!("<div class=\"figure\">{}</div>", draw_with(font, VARIED, &narrow));
let bold = Style {line_width: 0.1, double_gap: 0.26, ..Style::default()};
println!("<div class=\"figure\">{}</div>", draw_with(font, VARIED, &bold));
println!("</div>");
}
fn draw(font: &Font, source: &str) -> String {
match render_svg(source, font) {
Ok(markup) => markup,
Err(reason) => escape(&reason.to_string()),
}
}
fn draw_with(font: &Font, source: &str, style: &Style) -> String {
match render_svg_with(source, font, style, Shape::Outline) {
Ok(markup) => markup,
Err(reason) => escape(&reason.to_string()),
}
}