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 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 _ => {}
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 nums.push(0.0.into());
133 nums.push(self.page_width.into());
134 nums.sort();
137
138 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 (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 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 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 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 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 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 {
228 let lefty = &max_right_for_row[top..bottom];
229 let mut begin = 0;
230 let mut end = 0;
231
232 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 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 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 )));
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 )));
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 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 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 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}