Registry / RepositoryTypeScriptRustPython
Rollpie
ReadmeFiles
Versions
Info
Download
0.2.0579.9 KB2026-09-150.1.1576.6 KB2026-09-12
Version
0.1.1
Copyright
Rollpie, Irohabook
Publisher
math
Published
2026-09-12
Size
576.6 KB
Downloads
4
Checksum
8e0181e42f4abdc9f91c1ff48b058761bf1940be8d2d4747e5e74bd3014f4322
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
//! 数式のサンプルを 1 枚の HTML ページにして標準出力へ書き出す
//!
//! 使い方:
//!   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 のあとにフォントのファイルを指定してください");
            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!("フォントを読めません: {path}: {reason}");
            std::process::exit(1);
        }
    };
    let font = match font::load(data) {
        Ok(font) => font,
        Err(reason) => {
            eprintln!("フォントを読み込めません: {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=\"ja\">");
    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>書体は {} です。同じ式を 3 通りで組んでいます。HTML は文字を並べたもの、\
SVG 文字は SVG の中に文字を置いたもの、SVG 輪郭は字形を輪郭で描いたものです。\
輪郭は表示する側にフォントが要りません。文中に置くとこう見えます: {} ここまで。</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 文字", &render_svg(formula, font, Mode::Display, Shape::Text));
        write_render("SVG 輪郭", &render_svg(formula, font, Mode::Display, Shape::Outline));
        println!("</div>");
        println!("</div>");
    }

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


/// 1 つの組み方の結果を書き出す
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\">エラー: {}</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()),
    }
}