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