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

wasm.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
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
//! Entry points for calling from JavaScript. Included only when building for wasm32
//!
//! Strings are passed as UTF-8 bytes. The caller writes into memory obtained from allocate,
//! calls the function, and frees the memory with deallocate when done. Each function returns 0
//! on success and 1 on failure. The result is placed where resultPointer and resultLength point,
//! and on failure it holds the error message. The next call overwrites the result, so read it
//! right after each call.
//!
//! The exported names are in camelCase, as is usual in JavaScript.

use crate::Mode;
use crate::font::{self, Font};
use crate::layout::{self, Content, Layout, Shape};
use crate::node::Style;
use crate::outline;
use crate::parse;
use crate::svg::DARKEN;
use crate::text::number;
use std::cell::RefCell;


thread_local! {
    /// The loaded font. Empty until loadFont is called
    static FONT: RefCell<Option<Font>> = const { RefCell::new(None) };

    /// The result of the last call
    static RESULT: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
}


/// Allocates length bytes of memory for passing data
#[unsafe(no_mangle)]
pub extern "C" fn allocate(length: usize) -> *mut u8 {
    let mut buffer: Vec<u8> = Vec::with_capacity(length);
    let pointer = buffer.as_mut_ptr();
    std::mem::forget(buffer);

    pointer
}


/// Frees memory obtained from allocate
///
/// # Safety
///
/// pointer must be what allocate returned, and length must be the length passed to that call
#[unsafe(no_mangle)]
pub unsafe extern "C" fn deallocate(pointer: *mut u8, length: usize) {
    drop(unsafe { Vec::from_raw_parts(pointer, 0, length) });
}


/// Where the result of the last call is stored
#[unsafe(export_name = "resultPointer")]
pub extern "C" fn result_pointer() -> *const u8 {
    RESULT.with(|slot| slot.borrow().as_ptr())
}


/// The length of the result of the last call (in bytes)
#[unsafe(export_name = "resultLength")]
pub extern "C" fn result_length() -> usize {
    RESULT.with(|slot| slot.borrow().len())
}


/// Loads a font. A font loaded earlier is discarded
///
/// # Safety
///
/// pointer must point to length bytes that can be read
#[unsafe(export_name = "loadFont")]
pub unsafe extern "C" fn load_font(pointer: *const u8, length: usize) -> u32 {
    let data = unsafe { std::slice::from_raw_parts(pointer, length) }.to_vec();

    finish(match font::load(data) {
        Ok(loaded) => {
            FONT.with(|slot| *slot.borrow_mut() = Some(loaded));

            Ok(String::new())
        }
        // The position is a byte offset in the font file, not a character position,
        // so only the message is returned
        Err(reason) => Err(reason.message),
    })
}


/// Turns a formula into an SVG of outlines. If display is not 0, the display style is used.
/// If the formula has a wrapper, the wrapper decides the style
///
/// # Safety
///
/// pointer must point to length bytes that can be read
#[unsafe(export_name = "renderSvg")]
pub unsafe extern "C" fn render_svg(pointer: *const u8, length: usize, display: u32) -> u32 {
    let source = match unsafe { read_text(pointer, length) } {
        Ok(source) => source,
        Err(reason) => return finish(Err(reason)),
    };

    finish(with_font(|loaded| {
        crate::render_svg(&source, loaded, mode_of(display), Shape::Outline)
            .map_err(|reason| reason.to_string())
    }))
}


/// Turns the layout for drawing a formula on a canvas into JSON. display works as in renderSvg
///
/// All dimensions and positions are in em, with y positive downward and the baseline at 0.
/// Glyphs are given by their glyph IDs in the font, and their outlines are obtained separately
/// from glyphOutline. Characters missing from the typeface have no outline, so they are
/// returned as text
///
/// # Safety
///
/// pointer must point to length bytes that can be read
#[unsafe(export_name = "layoutNotation")]
pub unsafe extern "C" fn layout_notation(pointer: *const u8, length: usize, display: u32) -> u32 {
    let source = match unsafe { read_text(pointer, length) } {
        Ok(source) => source,
        Err(reason) => return finish(Err(reason)),
    };

    finish(with_font(|loaded| {
        let (node, wrapper) = parse::parse_wrapped(&source).map_err(|reason| reason.to_string())?;
        let shown = parse::display_of(wrapper, display != 0);
        let placed = layout::layout(&node, loaded, 1.0, Style::Auto, shown);

        Ok(notation_json(&placed, loaded))
    }))
}


/// Turns the outline of a glyph into the d attribute of an SVG path. Coordinates stay in font
/// units, and y points downward
#[unsafe(export_name = "glyphOutline")]
pub extern "C" fn glyph_outline(glyph: u32) -> u32 {
    let Ok(glyph) = u16::try_from(glyph) else {
        return finish(Err(format!("glyph ID is too large: {glyph}")));
    };

    finish(with_font(|loaded| {
        Ok(outline::path_data(&font::outline_of(loaded, glyph), 1.0, 0.0, 0.0))
    }))
}


/// The thickness (in em) of the stroke drawn around outlines. With this stroke, glyphs look as
/// thick as in the SVG output
#[unsafe(no_mangle)]
pub extern "C" fn darken() -> f64 {
    DARKEN
}


/// Reads the string that the caller wrote
///
/// # Safety
///
/// pointer must point to length bytes that can be read
unsafe fn read_text(pointer: *const u8, length: usize) -> Result<String, String> {
    let bytes = unsafe { std::slice::from_raw_parts(pointer, length) };

    match std::str::from_utf8(bytes) {
        Ok(text) => Ok(text.to_string()),
        Err(_) => Err("the string passed in is not UTF-8".to_string()),
    }
}


/// Stores the result, then returns 0 on success and 1 on failure
fn finish(outcome: Result<String, String>) -> u32 {
    let (status, text) = match outcome {
        Ok(text) => (0, text),
        Err(text) => (1, text),
    };

    RESULT.with(|slot| *slot.borrow_mut() = text.into_bytes());

    status
}


/// Typesets with the loaded font. Fails if no font has been loaded yet
fn with_font(work: impl FnOnce(&Font) -> Result<String, String>) -> Result<String, String> {
    FONT.with(|slot| match slot.borrow().as_ref() {
        Some(loaded) => work(loaded),
        None => Err("called before a font was loaded. Call loadFont first".to_string()),
    })
}


fn mode_of(display: u32) -> Mode {
    if display == 0 {Mode::Inline} else {Mode::Display}
}


/// Turns a typeset group into JSON. Glyphs, rules and text are each listed in drawing order
fn notation_json(placed: &Layout, font: &Font) -> String {
    let mut glyph_out = String::new();
    let mut rule_out = String::new();
    let mut text_out = String::new();

    collect(placed, font, 0.0, 0.0, &mut glyph_out, &mut rule_out, &mut text_out);

    format!(
        "{{\"width\":{},\"height\":{},\"depth\":{},\"glyph\":[{glyph_out}],\"rule\":[{rule_out}],\"text\":[{text_out}]}}",
        number(placed.width),
        number(placed.height),
        number(placed.depth)
    )
}


/// Traverses a group and appends its glyphs, rules and text to their lists. x and y are the
/// position of the group
fn collect(
    placed: &Layout,
    font: &Font,
    x: f64,
    y: f64,
    glyph_out: &mut String,
    rule_out: &mut String,
    text_out: &mut String,
) {
    match &placed.content {
        Content::Glyph {text, glyph, size, stretch, ..} => {
            // A character missing from the typeface (such as Japanese in \text) has no outline
            // to draw, so it is placed as text
            if *glyph == 0 {
                if text.is_empty() {
                    return;
                }

                separate(text_out);
                text_out.push_str("{\"value\":");
                push_string(text_out, text);
                text_out.push_str(&format!(
                    ",\"size\":{},\"offsetX\":{},\"offsetY\":{}}}",
                    number(*size),
                    number(x),
                    number(y)
                ));

                return;
            }

            // A glyph with an empty outline (such as a space) has nothing to draw. The SVG output
            // skips it too
            if font::outline_of(font, *glyph).segment.is_empty() {
                return;
            }

            let scale = size / font::units_per_em(font);
            separate(glyph_out);
            glyph_out.push_str(&format!(
                "{{\"glyph\":{glyph},\"scaleX\":{},\"scaleY\":{},\"offsetX\":{},\"offsetY\":{}}}",
                number(scale),
                number(scale * stretch),
                number(x),
                number(y)
            ));
        }
        Content::Rule => {
            separate(rule_out);
            rule_out.push_str(&format!(
                "{{\"offsetX\":{},\"offsetY\":{},\"width\":{},\"height\":{}}}",
                number(x),
                number(y - placed.height),
                number(placed.width),
                number(placed.height + placed.depth)
            ));
        }
        Content::Group(children) => {
            for child in children {
                collect(&child.layout, font, x + child.x, y + child.y, glyph_out, rule_out, text_out);
            }
        }
    }
}


/// When adding the second or a later item to a list, puts a separator before it
fn separate(out: &mut String) {
    if !out.is_empty() {
        out.push(',');
    }
}


/// Appends value to out as a JSON string
fn push_string(out: &mut String, value: &str) {
    out.push('"');

    for character in value.chars() {
        match character {
            '"' => out.push_str("\\\""),
            '\\' => out.push_str("\\\\"),
            control if (control as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", control as u32)),
            other => out.push(other),
        }
    }

    out.push('"');
}