1use std::any::Any;
10use std::collections::HashMap;
11use std::sync::Arc;
12
13use frontend::commands::block_commands;
14
15use crate::flow::FragmentContent;
16use crate::inner::TextDocumentInner;
17use crate::{CharVerticalAlignment, Color, TextFormat, UnderlineStyle};
18
19#[derive(Debug, Clone, Default, PartialEq, Eq)]
29pub struct HighlightFormat {
30 pub foreground_color: Option<Color>,
31 pub background_color: Option<Color>,
32 pub underline_color: Option<Color>,
33 pub font_family: Option<String>,
34 pub font_point_size: Option<u32>,
35 pub font_weight: Option<u32>,
36 pub font_bold: Option<bool>,
37 pub font_italic: Option<bool>,
38 pub font_underline: Option<bool>,
39 pub font_overline: Option<bool>,
40 pub font_strikeout: Option<bool>,
41 pub letter_spacing: Option<i32>,
42 pub word_spacing: Option<i32>,
43 pub underline_style: Option<UnderlineStyle>,
44 pub vertical_alignment: Option<CharVerticalAlignment>,
45 pub tooltip: Option<String>,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct HighlightSpan {
53 pub start: usize,
54 pub length: usize,
55 pub format: HighlightFormat,
56}
57
58pub struct HighlightContext {
62 spans: Vec<HighlightSpan>,
63 previous_state: i64,
64 current_state: i64,
65 block_id: usize,
66 user_data: Option<Box<dyn Any + Send + Sync>>,
67}
68
69impl HighlightContext {
70 pub fn new(
72 block_id: usize,
73 previous_state: i64,
74 user_data: Option<Box<dyn Any + Send + Sync>>,
75 ) -> Self {
76 Self {
77 spans: Vec::new(),
78 previous_state,
79 current_state: -1,
80 block_id,
81 user_data,
82 }
83 }
84
85 pub fn set_format(&mut self, start: usize, length: usize, format: HighlightFormat) {
89 if length == 0 {
90 return;
91 }
92 self.spans.push(HighlightSpan {
93 start,
94 length,
95 format,
96 });
97 }
98
99 pub fn previous_block_state(&self) -> i64 {
101 self.previous_state
102 }
103
104 pub fn set_current_block_state(&mut self, state: i64) {
109 self.current_state = state;
110 }
111
112 pub fn current_block_state(&self) -> i64 {
114 self.current_state
115 }
116
117 pub fn block_id(&self) -> usize {
119 self.block_id
120 }
121
122 pub fn set_user_data(&mut self, data: Box<dyn Any + Send + Sync>) {
124 self.user_data = Some(data);
125 }
126
127 pub fn user_data(&self) -> Option<&(dyn Any + Send + Sync)> {
129 self.user_data.as_deref()
130 }
131
132 pub fn user_data_mut(&mut self) -> Option<&mut (dyn Any + Send + Sync)> {
134 self.user_data.as_deref_mut()
135 }
136
137 pub fn into_parts(self) -> (Vec<HighlightSpan>, i64, Option<Box<dyn Any + Send + Sync>>) {
140 (self.spans, self.current_state, self.user_data)
141 }
142}
143
144pub trait SyntaxHighlighter: Send + Sync {
155 fn highlight_block(&self, text: &str, ctx: &mut HighlightContext);
157}
158
159pub(crate) struct BlockHighlightData {
165 pub spans: Vec<HighlightSpan>,
166 pub state: i64,
167 pub user_data: Option<Box<dyn Any + Send + Sync>>,
168}
169
170pub(crate) struct HighlightData {
172 pub highlighter: Arc<dyn SyntaxHighlighter>,
173 pub blocks: HashMap<usize, BlockHighlightData>,
174}
175
176fn apply_highlight(base: &TextFormat, hl: &HighlightFormat) -> TextFormat {
182 TextFormat {
183 font_family: hl.font_family.clone().or_else(|| base.font_family.clone()),
184 font_point_size: hl.font_point_size.or(base.font_point_size),
185 font_weight: hl.font_weight.or(base.font_weight),
186 font_bold: hl.font_bold.or(base.font_bold),
187 font_italic: hl.font_italic.or(base.font_italic),
188 font_underline: hl.font_underline.or(base.font_underline),
189 font_overline: hl.font_overline.or(base.font_overline),
190 font_strikeout: hl.font_strikeout.or(base.font_strikeout),
191 letter_spacing: hl.letter_spacing.or(base.letter_spacing),
192 word_spacing: hl.word_spacing.or(base.word_spacing),
193 underline_style: hl
194 .underline_style
195 .clone()
196 .or_else(|| base.underline_style.clone()),
197 vertical_alignment: hl
198 .vertical_alignment
199 .clone()
200 .or_else(|| base.vertical_alignment.clone()),
201 tooltip: hl.tooltip.clone().or_else(|| base.tooltip.clone()),
202 foreground_color: hl.foreground_color.or(base.foreground_color),
203 background_color: hl.background_color.or(base.background_color),
204 underline_color: hl.underline_color.or(base.underline_color),
205 anchor_href: base.anchor_href.clone(),
207 anchor_names: base.anchor_names.clone(),
208 is_anchor: base.is_anchor,
209 }
210}
211
212fn merge_overlapping_highlights(spans: &[&HighlightSpan]) -> HighlightFormat {
215 let mut merged = HighlightFormat::default();
216 for span in spans {
217 let f = &span.format;
218 if f.foreground_color.is_some() {
219 merged.foreground_color = f.foreground_color;
220 }
221 if f.background_color.is_some() {
222 merged.background_color = f.background_color;
223 }
224 if f.underline_color.is_some() {
225 merged.underline_color = f.underline_color;
226 }
227 if f.font_family.is_some() {
228 merged.font_family = f.font_family.clone();
229 }
230 if f.font_point_size.is_some() {
231 merged.font_point_size = f.font_point_size;
232 }
233 if f.font_weight.is_some() {
234 merged.font_weight = f.font_weight;
235 }
236 if f.font_bold.is_some() {
237 merged.font_bold = f.font_bold;
238 }
239 if f.font_italic.is_some() {
240 merged.font_italic = f.font_italic;
241 }
242 if f.font_underline.is_some() {
243 merged.font_underline = f.font_underline;
244 }
245 if f.font_overline.is_some() {
246 merged.font_overline = f.font_overline;
247 }
248 if f.font_strikeout.is_some() {
249 merged.font_strikeout = f.font_strikeout;
250 }
251 if f.letter_spacing.is_some() {
252 merged.letter_spacing = f.letter_spacing;
253 }
254 if f.word_spacing.is_some() {
255 merged.word_spacing = f.word_spacing;
256 }
257 if f.underline_style.is_some() {
258 merged.underline_style = f.underline_style.clone();
259 }
260 if f.vertical_alignment.is_some() {
261 merged.vertical_alignment = f.vertical_alignment.clone();
262 }
263 if f.tooltip.is_some() {
264 merged.tooltip = f.tooltip.clone();
265 }
266 }
267 merged
268}
269
270fn compute_word_starts_local(text: &str) -> Vec<u8> {
281 use unicode_segmentation::UnicodeSegmentation;
282 let mut result = Vec::new();
283 let mut byte_to_char: Vec<(usize, usize)> = Vec::new();
284 for (ci, (bi, _)) in text.char_indices().enumerate() {
285 byte_to_char.push((bi, ci));
286 }
287 for (byte_off, _word) in text.unicode_word_indices() {
288 let char_idx = byte_to_char
289 .iter()
290 .find(|(bi, _)| *bi == byte_off)
291 .map(|(_, ci)| *ci)
292 .unwrap_or(0);
293 if let Ok(idx) = u8::try_from(char_idx) {
294 result.push(idx);
295 } else {
296 break;
297 }
298 }
299 result
300}
301
302pub(crate) fn merge_highlight_spans(
303 fragments: Vec<FragmentContent>,
304 spans: &[HighlightSpan],
305) -> Vec<FragmentContent> {
306 if spans.is_empty() {
307 return fragments;
308 }
309
310 let mut result = Vec::with_capacity(fragments.len());
311
312 for frag in fragments {
313 match frag {
314 FragmentContent::Text {
315 ref text,
316 ref format,
317 offset,
318 length,
319 element_id,
320 word_starts: _,
321 } => {
322 let frag_end = offset + length;
323
324 let mut boundaries = Vec::new();
326 boundaries.push(offset);
327 boundaries.push(frag_end);
328
329 for span in spans {
330 let span_end = span.start + span.length;
331 if span.start < frag_end && span_end > offset {
333 if span.start > offset && span.start < frag_end {
334 boundaries.push(span.start);
335 }
336 if span_end > offset && span_end < frag_end {
337 boundaries.push(span_end);
338 }
339 }
340 }
341
342 boundaries.sort_unstable();
343 boundaries.dedup();
344
345 let chars: Vec<char> = text.chars().collect();
347 for window in boundaries.windows(2) {
348 let sub_start = window[0];
349 let sub_end = window[1];
350 let sub_len = sub_end - sub_start;
351 if sub_len == 0 {
352 continue;
353 }
354
355 let active: Vec<&HighlightSpan> = spans
357 .iter()
358 .filter(|s| {
359 let s_end = s.start + s.length;
360 s.start < sub_end && s_end > sub_start
361 })
362 .collect();
363
364 let char_start = sub_start - offset;
365 let char_end = char_start + sub_len;
366 let sub_text: String = chars[char_start..char_end].iter().collect();
367
368 let sub_format = if active.is_empty() {
369 format.clone()
370 } else {
371 let merged_hl = merge_overlapping_highlights(&active);
372 apply_highlight(format, &merged_hl)
373 };
374
375 let sub_word_starts = compute_word_starts_local(&sub_text);
376 result.push(FragmentContent::Text {
377 text: sub_text,
378 format: sub_format,
379 offset: sub_start,
380 length: sub_len,
381 element_id,
394 word_starts: sub_word_starts,
395 });
396 }
397 }
398 FragmentContent::Image {
399 ref name,
400 width,
401 height,
402 quality,
403 ref format,
404 offset,
405 element_id,
406 } => {
407 let active: Vec<&HighlightSpan> = spans
409 .iter()
410 .filter(|s| {
411 let s_end = s.start + s.length;
412 s.start < offset + 1 && s_end > offset
413 })
414 .collect();
415
416 let img_format = if active.is_empty() {
417 format.clone()
418 } else {
419 let merged_hl = merge_overlapping_highlights(&active);
420 apply_highlight(format, &merged_hl)
421 };
422
423 result.push(FragmentContent::Image {
424 name: name.clone(),
425 width,
426 height,
427 quality,
428 format: img_format,
429 offset,
430 element_id,
431 });
432 }
433 }
434 }
435
436 result
437}
438
439fn ordered_block_ids(inner: &TextDocumentInner) -> Vec<(u64, String)> {
445 let mut blocks = block_commands::get_all_block(&inner.ctx).unwrap_or_default();
446 blocks.sort_by_key(|b| b.document_position);
447 blocks.into_iter().map(|b| (b.id, b.plain_text)).collect()
448}
449
450impl TextDocumentInner {
451 pub(crate) fn rehighlight_all(&mut self) {
453 let hl = match self.highlight {
454 Some(ref mut hl) => hl,
455 None => return,
456 };
457
458 let highlighter = Arc::clone(&hl.highlighter);
459 hl.blocks.clear();
460
461 let blocks = ordered_block_ids(self);
462 let mut previous_state: i64 = -1;
463
464 for (block_id, text) in &blocks {
465 let bid = *block_id as usize;
466 let mut ctx = HighlightContext::new(bid, previous_state, None);
467 highlighter.highlight_block(text, &mut ctx);
468 let (spans, state, user_data) = ctx.into_parts();
469
470 previous_state = state;
471
472 let hl = self.highlight.as_mut().unwrap();
474 hl.blocks.insert(
475 bid,
476 BlockHighlightData {
477 spans,
478 state,
479 user_data,
480 },
481 );
482 }
483 }
484
485 pub(crate) fn rehighlight_from_block(&mut self, start_block_id: usize) {
488 let hl = match self.highlight {
489 Some(ref hl) => hl,
490 None => return,
491 };
492
493 let highlighter = Arc::clone(&hl.highlighter);
494 let blocks = ordered_block_ids(self);
495
496 let start_idx = match blocks
498 .iter()
499 .position(|(id, _)| *id as usize == start_block_id)
500 {
501 Some(idx) => idx,
502 None => return,
503 };
504
505 for i in start_idx..blocks.len() {
506 let (block_id, ref text) = blocks[i];
507 let bid = block_id as usize;
508
509 let hl = self.highlight.as_ref().unwrap();
510
511 let previous_state = if i == 0 {
513 -1
514 } else {
515 let prev_bid = blocks[i - 1].0 as usize;
516 hl.blocks.get(&prev_bid).map_or(-1, |d| d.state)
517 };
518
519 let user_data = self
521 .highlight
522 .as_mut()
523 .unwrap()
524 .blocks
525 .get_mut(&bid)
526 .and_then(|d| d.user_data.take());
527
528 let old_state = self
529 .highlight
530 .as_ref()
531 .unwrap()
532 .blocks
533 .get(&bid)
534 .map_or(-1, |d| d.state);
535
536 let mut ctx = HighlightContext::new(bid, previous_state, user_data);
537 highlighter.highlight_block(text, &mut ctx);
538 let (spans, state, user_data) = ctx.into_parts();
539
540 let hl = self.highlight.as_mut().unwrap();
541 hl.blocks.insert(
542 bid,
543 BlockHighlightData {
544 spans,
545 state,
546 user_data,
547 },
548 );
549
550 if i > start_idx && state == old_state {
553 break;
554 }
555 }
556 }
557
558 pub(crate) fn rehighlight_affected(&mut self, position: usize) {
561 if self.highlight.is_none() {
562 return;
563 }
564
565 let blocks = ordered_block_ids(self);
566
567 let target_bid = blocks
569 .iter()
570 .rev()
571 .find_map(|(id, _)| {
572 let dto = block_commands::get_block(&self.ctx, id).ok().flatten()?;
573 let bp = dto.document_position as usize;
574 if position >= bp {
575 Some(*id as usize)
576 } else {
577 None
578 }
579 })
580 .unwrap_or_else(|| blocks.first().map_or(0, |(id, _)| *id as usize));
581
582 if blocks.is_empty() {
583 return;
584 }
585
586 self.rehighlight_from_block(target_bid);
587 }
588}