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 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 _ => {}
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 nums.push(0.0.into());
130 nums.push(self.page_width.into());
131 nums.sort();
134
135 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 (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 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 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 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 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 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 {
225 let lefty = &max_right_for_row[top..bottom];
226 let mut begin = 0;
227 let mut end = 0;
228
229 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 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 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 )));
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 )));
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 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 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 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}