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

main.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
//! 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()),
    }
}