Skip to main content

reflexo_vec2sema/
lib.rs

1mod incr;
2
3use std::{
4    borrow::Cow,
5    collections::{BTreeMap, VecDeque},
6};
7
8use reflexo::{
9    escape::{self, escape_str, AttributeEscapes, PcDataEscapes},
10    hash::Fingerprint,
11    vector::ir::{self, Module, Point, Rect, Scalar, VecItem},
12};
13use reflexo_vec2canvas::BrowserFontMetric;
14use unicode_width::UnicodeWidthChar;
15
16pub use incr::*;
17
18pub struct SemaTask {
19    heavy: bool,
20    font_metric: BrowserFontMetric,
21    page_width: f32,
22    page_height: f32,
23    dfn_count: usize,
24    rects: Vec<(Fingerprint, Rect)>,
25    discrete_label_map: BTreeMap<Scalar, usize>,
26    discrete_value_map: Vec<Scalar>,
27}
28
29const EPS: f32 = 1e-3;
30
31impl SemaTask {
32    pub fn new(heavy: bool, font_metric: BrowserFontMetric, width: f32, height: f32) -> Self {
33        SemaTask {
34            heavy,
35            font_metric,
36            page_width: width,
37            page_height: height,
38            dfn_count: 0,
39            rects: vec![],
40            discrete_label_map: BTreeMap::new(),
41            discrete_value_map: vec![],
42        }
43    }
44
45    pub fn render_semantics<'a>(
46        &mut self,
47        ctx: &'a Module,
48        ts: tiny_skia::Transform,
49        fg: Fingerprint,
50        output: &mut Vec<Cow<'a, str>>,
51    ) {
52        self.prepare_text_rects(ctx, ts, fg);
53        self.prepare_discrete_map();
54        let mut fallbacks = self.calc_text_item_fallbacks();
55        self.dfn_count = 0;
56        self.render_semantics_walk(ctx, ts, fg, &mut fallbacks, output);
57    }
58
59    fn prepare_text_rects(&mut self, ctx: &Module, ts: tiny_skia::Transform, fg: Fingerprint) {
60        let item = ctx.get_item(&fg).unwrap();
61        use VecItem::*;
62        match item {
63            Group(t) => {
64                for (pos, child) in t.0.iter() {
65                    let ts = ts.pre_translate(pos.x.0, pos.y.0);
66                    self.prepare_text_rects(ctx, ts, *child);
67                }
68            }
69            Item(t) => {
70                let trans = t.0.clone();
71                let trans: ir::Transform = trans.into();
72                let ts = ts.pre_concat(trans.into());
73                self.prepare_text_rects(ctx, ts, t.1);
74            }
75            Labelled(t) => {
76                self.prepare_text_rects(ctx, ts, t.1);
77            }
78            Text(t) => {
79                // main logic
80                let size = (t.shape.size) * Scalar(ts.sy);
81
82                let font = ctx.get_font(&t.shape.font).unwrap();
83                let cap_height = font.cap_height * size;
84                let width = t.width();
85
86                let tx = Scalar(ts.tx);
87                let ty = Scalar(ts.ty) - cap_height;
88                let ty2 = ty + size;
89                let tx2 = tx + width;
90
91                self.rects.push((
92                    fg,
93                    Rect {
94                        lo: Point { x: tx, y: ty },
95                        hi: Point { x: tx2, y: ty2 },
96                    },
97                ));
98            }
99            // todo
100            // Html(t) => {
101            //     // t.size
102
103            //     let tx = Scalar(ts.tx);
104            //     let ty = Scalar(ts.ty);
105
106            //     let tx2 = tx + Scalar(t.size.x.0 * ts.sx + t.size.y.0 * ts.kx);
107            //     let ty2 = ty + Scalar(t.size.x.0 * ts.ky + t.size.y.0 * ts.sy);
108
109            //     self.rects.push((
110            //         fg,
111            //         Rect {
112            //             lo: Point { x: tx, y: ty },
113            //             hi: Point { x: tx2, y: ty2 },
114            //         },
115            //     ));
116            // }
117            _ => {}
118        }
119    }
120
121    fn prepare_discrete_map(&mut self) {
122        let nums = &mut self.discrete_value_map;
123
124        for (_, rect) in self.rects.iter() {
125            nums.push(rect.lo.x);
126            nums.push(rect.lo.y);
127            nums.push(rect.hi.x);
128            nums.push(rect.hi.y);
129        }
130
131        // page borders
132        nums.push(0.0.into());
133        nums.push(self.page_width.into());
134        // todo: page height
135
136        nums.sort();
137
138        // unique label for f32 pairs
139        struct DiscreteState {
140            label: usize,
141            last: Scalar,
142        }
143        let mut state = Option::<DiscreteState>::None;
144
145        fn approx_eq(a: f32, b: f32) -> bool {
146            // todo: use transform-aware epsilon
147            (a - b).abs() < EPS
148        }
149
150        for (idx, &mut num) in nums.iter_mut().enumerate() {
151            if let Some(state) = state.as_mut() {
152                if !approx_eq(state.last.0, num.0) {
153                    state.label = idx;
154                }
155            } else {
156                state = Some(DiscreteState {
157                    label: idx,
158                    last: num,
159                });
160            }
161            let state = state.as_mut().unwrap();
162            self.discrete_label_map.insert(num, state.label);
163            state.last = num;
164        }
165    }
166
167    // Vec<(prepend: String, append: String)>
168    fn calc_text_item_fallbacks(&mut self) -> VecDeque<(String, String)> {
169        let mut res = VecDeque::new();
170        res.resize(self.rects.len(), (String::new(), String::new()));
171
172        // Append right and bottom fallbacks
173        for (idx, (_, rect)) in self.rects.iter().enumerate() {
174            let left = rect.lo.x;
175            let top = rect.lo.y;
176            let right = rect.hi.x;
177            let bottom = rect.hi.y;
178
179            res[idx].1.push_str(&format!(
180                r#"<span class="typst-content-fallback typst-content-fallback-rb1" style="left: calc(var(--data-text-width) * {:.5}); top: calc(var(--data-text-height) * {:.5}); width: calc(var(--data-text-width) * {:.5}); height: calc(var(--data-text-height) * {:.5});"></span>"#,
181                left.0,
182                bottom.0,
183                self.page_width - left.0,
184                self.page_height - bottom.0,
185            ));
186
187            res[idx].1.push_str(&format!(
188                r#"<span class="typst-content-fallback typst-content-fallback-rb2" style="left: calc(var(--data-text-width) * {:.5}); top: calc(var(--data-text-height) * {:.5}); width: calc(var(--data-text-width) * {:.5}); height: calc(var(--data-text-height) * {:.5});"></span>"#,
189                right.0,
190                top.0,
191                self.page_width - right.0,
192                self.page_height - top.0,
193            ));
194        }
195
196        let zero_label = *self.discrete_label_map.get(&Scalar(0.0)).unwrap();
197        let mut last_bottom = zero_label;
198
199        // todo: optimize using ds
200        let mut max_right_for_row = Vec::<Option<usize>>::new();
201        max_right_for_row.resize(self.discrete_value_map.len(), None);
202        let mut max_bottom_for_col = Vec::<Option<usize>>::new();
203        max_bottom_for_col.resize(self.discrete_value_map.len(), None);
204
205        // Prepend left and top fallbacks
206        for post_idx in 1..self.rects.len() {
207            let pre_idx = post_idx - 1;
208            let (_, rect) = self.rects[pre_idx];
209
210            let (left, top, right, bottom) = self.get_discrete_labels_for_text_item(rect);
211
212            // Prepend whole width blanks
213            if top > last_bottom {
214                let from = self.discrete_value_map[last_bottom];
215                let height = rect.lo.y - from;
216                res[pre_idx].0.push_str(&format!(
217                    r#"<span class="typst-content-fallback typst-content-fallback-whole" style="left: calc(var(--data-text-width) * {:.5}); top: calc(var(--data-text-height) * {:.5}); width: calc(var(--data-text-width) * {:.5}); height: calc(var(--data-text-height) * {:.5});"></span>"#,
218                    0.0,
219                    from.0,
220                    self.page_width,
221                    height.0,
222                ));
223            }
224            last_bottom = last_bottom.max(bottom);
225
226            // Process current item left
227            {
228                let lefty = &max_right_for_row[top..bottom];
229                let mut begin = 0;
230                let mut end = 0;
231
232                // group by contiguous same value
233                while begin < lefty.len() {
234                    while end < lefty.len() && lefty[begin] == lefty[end] {
235                        end += 1;
236                    }
237
238                    let last_right =
239                        lefty[begin].and_then(|v| if v > left { None } else { Some(v) });
240
241                    // if last_right.is_none() && begin+top <=
242
243                    // expand to page border 0.0 if no last right
244                    let from = match last_right {
245                        Some(last_right) => {
246                            (self.discrete_value_map[last_right] + rect.lo.x) / Scalar(2.0)
247                        }
248                        None => Scalar(0.0),
249                    };
250                    let width = rect.lo.x - from;
251
252                    let ptop = if last_right.is_none() {
253                        (begin + top).max(last_bottom)
254                    } else {
255                        begin + top
256                    };
257                    let pbottom = (end + top).min(bottom);
258
259                    if ptop < pbottom {
260                        let ptop = self.discrete_value_map[ptop];
261                        let pbottom = self.discrete_value_map[pbottom];
262
263                        res[pre_idx].0.push_str(&format!(
264                            r#"<span class="typst-content-fallback typst-content-fallback-left" style="left: calc(var(--data-text-width) * {:.5}); top: calc(var(--data-text-height) * {:.5}); width: calc(var(--data-text-width) * {:.5}); height: calc(var(--data-text-height) * {:.5});"></span>"#,
265                            from.0,
266                            ptop.0,
267                            width.0,
268                            pbottom.0 - ptop.0,
269                        ));
270                    }
271
272                    begin = end;
273                }
274
275                // maintain max_right_for_row
276                for elem in &mut max_right_for_row[top..bottom] {
277                    let val = elem.get_or_insert(right);
278                    *val = right.max(*val);
279                }
280            }
281        }
282
283        res
284    }
285
286    fn get_discrete_labels_for_text_item(&self, rect: Rect) -> (usize, usize, usize, usize) {
287        let mut left = *self.discrete_label_map.get(&rect.lo.x).unwrap();
288        let mut top = *self.discrete_label_map.get(&rect.lo.y).unwrap();
289        let mut right = *self.discrete_label_map.get(&rect.hi.x).unwrap();
290        let mut bottom = *self.discrete_label_map.get(&rect.hi.y).unwrap();
291        if left > right {
292            std::mem::swap(&mut left, &mut right);
293        }
294        if top > bottom {
295            std::mem::swap(&mut top, &mut bottom);
296        }
297
298        (left, top, right, bottom)
299    }
300
301    fn render_semantics_walk<'a>(
302        &mut self,
303        ctx: &'a Module,
304        ts: tiny_skia::Transform,
305        fg: Fingerprint,
306        fallbacks: &mut VecDeque<(String, String)>,
307        output: &mut Vec<Cow<'a, str>>,
308    ) {
309        let item = ctx.get_item(&fg).unwrap();
310
311        use VecItem::*;
312        match item {
313            Group(t) => {
314                output.push(Cow::Borrowed(r#"<span class="typst-content-group">"#));
315                for (pos, child) in t.0.iter() {
316                    let ts = ts.pre_translate(pos.x.0, pos.y.0);
317                    self.render_semantics_walk(ctx, ts, *child, fallbacks, output);
318                }
319                output.push(Cow::Borrowed("</span>"));
320            }
321            Item(t) => {
322                output.push(Cow::Borrowed(r#"<span class="typst-content-group">"#));
323                let trans = t.0.clone();
324                let trans: ir::Transform = trans.into();
325                let ts = ts.pre_concat(trans.into());
326                self.render_semantics_walk(ctx, ts, t.1, fallbacks, output);
327                output.push(Cow::Borrowed("</span>"));
328            }
329            Labelled(t) => {
330                output.push(Cow::Borrowed(r#""#));
331                output.push(Cow::Owned(format!(
332                    r#"<span class="typst-content-group" data-typst-label="{}" >"#,
333                    escape_str::<AttributeEscapes>(&t.0)
334                )));
335                self.render_semantics_walk(ctx, ts, t.1, fallbacks, output);
336                output.push(Cow::Borrowed("</span>"));
337            }
338            Text(t) => {
339                let text_id = self.dfn_count;
340                self.dfn_count += 1;
341
342                let is_regular_scale = ts.sx == 1.0 && ts.sy == 1.0;
343                let is_regular_skew = ts.kx == 0.0 && ts.ky == 0.0;
344                let can_heavy = self.heavy;
345                let size = (t.shape.size) * Scalar(ts.sy);
346
347                let scale_x = t.width().0
348                    / (t.content
349                        .content
350                        .chars()
351                        .map(|e| match e.width().unwrap_or_default() {
352                            0 => 0.,
353                            1 => self.font_metric.semi_char_width,
354                            2 => self.font_metric.full_char_width,
355                            _ => self.font_metric.emoji_width,
356                        })
357                        .sum::<f32>()
358                        * size.0);
359
360                let (_, rect) = self.rects[text_id];
361
362                let (prepend, append) = fallbacks.pop_front().unwrap();
363
364                if can_heavy {
365                    output.push(Cow::Owned(prepend));
366                }
367
368                if is_regular_scale && is_regular_skew {
369                    output.push(Cow::Owned(format!(
370                        r#"<span class="typst-content-text" data-text-id="{}" style="font-size: calc(var(--data-text-height) * {:.5}); line-height: calc(var(--data-text-height) * {:.5}); left: calc(var(--data-text-width) * {:.5}); top: calc(var(--data-text-height) * {:.5}); transform: scaleX({:.5})">"#,
371                        text_id,
372                        size.0,
373                        size.0,
374                        rect.lo.x.0,
375                        rect.lo.y.0,
376                        scale_x,
377                        // scale_y,
378                    )));
379                } else {
380                    output.push(Cow::Owned(format!(
381                        r#"<span class="typst-content-text" data-text-id="{}" data-matrix="{:.5},{:.5},{:.5},{:.5}" style="font-size: {:.5}px; line-height: calc(var(--data-text-height) * {:.5}); left: calc(var(--data-text-width) * {:.5}); top: calc(var(--data-text-height) * {:.5}); transform: scaleX({:.5})">"#,
382                        text_id,
383                        ts.sx,
384                        ts.ky,
385                        ts.kx,
386                        ts.sy,
387                        size.0,
388                        size.0,
389                        rect.lo.x.0,
390                        rect.lo.y.0,
391                        scale_x,
392                        // scale_y,
393                    )));
394                }
395
396                output.push(escape::escape_str::<PcDataEscapes>(
397                    t.content.content.as_ref(),
398                ));
399                output.push(Cow::Borrowed("</span>"));
400
401                if can_heavy {
402                    output.push(Cow::Owned(append));
403                }
404            }
405            ContentHint(c) => {
406                if *c == '\n' {
407                    // elem.style.top = `calc(var(--data-text-height) * ${rrt})`;
408                    // elem.style.left = `calc(var(--data-text-width) * ${rrl})`;
409                    output.push(Cow::Borrowed(r#"<br class="typst-content-hint""#));
410                    let is_regular_scale = ts.sx == 1.0 && ts.sy == 1.0;
411                    let is_regular_skew = ts.kx == 0.0 && ts.ky == 0.0;
412                    if is_regular_scale && is_regular_skew {
413                        output.push(Cow::Owned(format!(
414                            r#" style="font-size: 0px; left: calc(var(--data-text-width) * {:.5}); top: calc(var(--data-text-height) * {:.5});">"#,
415                            ts.tx,ts.ty,
416                        )));
417                    } else {
418                        output.push(Cow::Owned(format!(
419                            r#" data-matrix="{:.5},{:.5},{:.5},{:.5}" style="font-size: 0px; left: calc(var(--data-text-width) * {:.5}); top: calc(var(--data-text-height) * {:.5});">"#,
420                            ts.sx, ts.ky, ts.kx, ts.sy,   ts.tx,ts.ty,
421                        )));
422                    }
423                    return;
424                }
425                output.push(Cow::Borrowed(r#"<span class="typst-content-hint""#));
426                let is_regular_scale = ts.sx == 1.0 && ts.sy == 1.0;
427                let is_regular_skew = ts.kx == 0.0 && ts.ky == 0.0;
428                if is_regular_scale && is_regular_skew {
429                    output.push(Cow::Owned(format!(
430                        r#" style="font-size: 0px; left: calc(var(--data-text-width) * {:.5}); top: calc(var(--data-text-height) * {:.5});">"#,
431                        ts.tx,ts.ty,
432                    )));
433                } else {
434                    output.push(Cow::Owned(format!(
435                        r#" data-matrix="{:.5},{:.5},{:.5},{:.5}" style="font-size: 0px; left: calc(var(--data-text-width) * {:.5}); top: calc(var(--data-text-height) * {:.5});">"#,
436                        ts.sx, ts.ky, ts.kx, ts.sy,   ts.tx,ts.ty,
437                    )));
438                }
439                let c = c.to_string();
440                let c = escape::escape_str::<PcDataEscapes>(&c).into_owned();
441                output.push(Cow::Owned(c));
442                output.push(Cow::Borrowed("</span>"));
443            }
444            Link(t) => {
445                let href_handler = if t.href.starts_with("@typst:") {
446                    let href = t.href.trim_start_matches("@typst:");
447                    format!(r##" onclick="{href}; return false""##)
448                } else {
449                    String::new()
450                };
451                output.push(Cow::Owned(format!(
452                    r#"<a class="typst-content-link" href="{}""#,
453                    if href_handler.is_empty() {
454                        escape::escape_str::<AttributeEscapes>(&t.href)
455                    } else {
456                        Cow::Borrowed("#")
457                    },
458                )));
459                if !href_handler.is_empty() {
460                    output.push(Cow::Owned(href_handler));
461                }
462                let is_regular_scale = ts.sx == 1.0 && ts.sy == 1.0;
463                let is_regular_skew = ts.kx == 0.0 && ts.ky == 0.0;
464                if is_regular_scale && is_regular_skew {
465                    output.push(Cow::Owned(format!(
466                        r#" style="font-size: 0px; left: calc(var(--data-text-width) * {:.5}); top: calc(var(--data-text-height) * {:.5});  width: calc(var(--data-text-width) * {:.5}); height: calc(var(--data-text-height) * {:.5});">"#,
467                        ts.tx - 1., ts.ty - 2., t.size.x.0 + 2., t.size.y.0 + 4.,
468                    )));
469                } else {
470                    output.push(Cow::Owned(format!(
471                        r#" data-matrix="{:.5},{:.5},{:.5},{:.5}" style="font-size: 0px; left: calc(var(--data-text-width) * {:.5}); top: calc(var(--data-text-height) * {:.5});  width: calc(var(--data-text-width) * {:.5}); height: calc(var(--data-text-height) * {:.5});">"#,
472                        ts.sx, ts.ky, ts.kx, ts.sy, ts.tx,ts.ty, t.size.x.0, t.size.y.0,
473                    )));
474                }
475                output.push(Cow::Borrowed("</a>"));
476            }
477            // todo: implement in svg
478            SizedRawHtml(h) => {
479                web_sys::console::log_1(&format!("Html: {}", h.html).into());
480                output.push(Cow::Borrowed(r#"<span class="typst-content-html""#));
481                let is_regular_scale = ts.sx == 1.0 && ts.sy == 1.0;
482                let is_regular_skew = ts.kx == 0.0 && ts.ky == 0.0;
483                // todo: correct zindex
484                if is_regular_scale && is_regular_skew {
485                    output.push(Cow::Owned(format!(
486                        r#" style="zindex: 3; left: calc(var(--data-text-width) * {:.5}); top: calc(var(--data-text-height) * {:.5});  width: calc(var(--data-text-width) * {:.5}); height: calc(var(--data-text-height) * {:.5});">"#,
487                        ts.tx, ts.ty, h.size.x.0, h.size.y.0,
488                    )));
489                } else {
490                    output.push(Cow::Owned(format!(
491                        r#" data-matrix="{:.5},{:.5},{:.5},{:.5}" style="zindex: 3; left: calc(var(--data-text-width) * {:.5}); top: calc(var(--data-text-height) * {:.5});  width: calc(var(--data-text-width) * {:.5}); height: calc(var(--data-text-height) * {:.5});">"#,
492                        ts.sx, ts.ky, ts.kx, ts.sy, ts.tx, ts.ty, h.size.x.0, h.size.y.0,
493                    )));
494                }
495                output.push(Cow::Owned(h.html.to_string()));
496                output.push(Cow::Borrowed("</span>"));
497            }
498            Image(..) | Path(..) => {}
499            None | ColorTransform(..) | Gradient(..) | Color32(..) | Pattern(..) | Html(..) => {}
500        }
501    }
502}