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
270pub(crate) fn merge_highlight_spans(
276 fragments: Vec<FragmentContent>,
277 spans: &[HighlightSpan],
278) -> Vec<FragmentContent> {
279 if spans.is_empty() {
280 return fragments;
281 }
282
283 let mut result = Vec::with_capacity(fragments.len());
284
285 for frag in fragments {
286 match frag {
287 FragmentContent::Text {
288 ref text,
289 ref format,
290 offset,
291 length,
292 } => {
293 let frag_end = offset + length;
294
295 let mut boundaries = Vec::new();
297 boundaries.push(offset);
298 boundaries.push(frag_end);
299
300 for span in spans {
301 let span_end = span.start + span.length;
302 if span.start < frag_end && span_end > offset {
304 if span.start > offset && span.start < frag_end {
305 boundaries.push(span.start);
306 }
307 if span_end > offset && span_end < frag_end {
308 boundaries.push(span_end);
309 }
310 }
311 }
312
313 boundaries.sort_unstable();
314 boundaries.dedup();
315
316 let chars: Vec<char> = text.chars().collect();
318 for window in boundaries.windows(2) {
319 let sub_start = window[0];
320 let sub_end = window[1];
321 let sub_len = sub_end - sub_start;
322 if sub_len == 0 {
323 continue;
324 }
325
326 let active: Vec<&HighlightSpan> = spans
328 .iter()
329 .filter(|s| {
330 let s_end = s.start + s.length;
331 s.start < sub_end && s_end > sub_start
332 })
333 .collect();
334
335 let char_start = sub_start - offset;
336 let char_end = char_start + sub_len;
337 let sub_text: String = chars[char_start..char_end].iter().collect();
338
339 let sub_format = if active.is_empty() {
340 format.clone()
341 } else {
342 let merged_hl = merge_overlapping_highlights(&active);
343 apply_highlight(format, &merged_hl)
344 };
345
346 result.push(FragmentContent::Text {
347 text: sub_text,
348 format: sub_format,
349 offset: sub_start,
350 length: sub_len,
351 });
352 }
353 }
354 FragmentContent::Image {
355 ref name,
356 width,
357 height,
358 quality,
359 ref format,
360 offset,
361 } => {
362 let active: Vec<&HighlightSpan> = spans
364 .iter()
365 .filter(|s| {
366 let s_end = s.start + s.length;
367 s.start < offset + 1 && s_end > offset
368 })
369 .collect();
370
371 let img_format = if active.is_empty() {
372 format.clone()
373 } else {
374 let merged_hl = merge_overlapping_highlights(&active);
375 apply_highlight(format, &merged_hl)
376 };
377
378 result.push(FragmentContent::Image {
379 name: name.clone(),
380 width,
381 height,
382 quality,
383 format: img_format,
384 offset,
385 });
386 }
387 }
388 }
389
390 result
391}
392
393fn ordered_block_ids(inner: &TextDocumentInner) -> Vec<(u64, String)> {
399 let mut blocks = block_commands::get_all_block(&inner.ctx).unwrap_or_default();
400 blocks.sort_by_key(|b| b.document_position);
401 blocks.into_iter().map(|b| (b.id, b.plain_text)).collect()
402}
403
404impl TextDocumentInner {
405 pub(crate) fn rehighlight_all(&mut self) {
407 let hl = match self.highlight {
408 Some(ref mut hl) => hl,
409 None => return,
410 };
411
412 let highlighter = Arc::clone(&hl.highlighter);
413 hl.blocks.clear();
414
415 let blocks = ordered_block_ids(self);
416 let mut previous_state: i64 = -1;
417
418 for (block_id, text) in &blocks {
419 let bid = *block_id as usize;
420 let mut ctx = HighlightContext::new(bid, previous_state, None);
421 highlighter.highlight_block(text, &mut ctx);
422 let (spans, state, user_data) = ctx.into_parts();
423
424 previous_state = state;
425
426 let hl = self.highlight.as_mut().unwrap();
428 hl.blocks.insert(
429 bid,
430 BlockHighlightData {
431 spans,
432 state,
433 user_data,
434 },
435 );
436 }
437 }
438
439 pub(crate) fn rehighlight_from_block(&mut self, start_block_id: usize) {
442 let hl = match self.highlight {
443 Some(ref hl) => hl,
444 None => return,
445 };
446
447 let highlighter = Arc::clone(&hl.highlighter);
448 let blocks = ordered_block_ids(self);
449
450 let start_idx = match blocks
452 .iter()
453 .position(|(id, _)| *id as usize == start_block_id)
454 {
455 Some(idx) => idx,
456 None => return,
457 };
458
459 for i in start_idx..blocks.len() {
460 let (block_id, ref text) = blocks[i];
461 let bid = block_id as usize;
462
463 let hl = self.highlight.as_ref().unwrap();
464
465 let previous_state = if i == 0 {
467 -1
468 } else {
469 let prev_bid = blocks[i - 1].0 as usize;
470 hl.blocks.get(&prev_bid).map_or(-1, |d| d.state)
471 };
472
473 let user_data = self
475 .highlight
476 .as_mut()
477 .unwrap()
478 .blocks
479 .get_mut(&bid)
480 .and_then(|d| d.user_data.take());
481
482 let old_state = self
483 .highlight
484 .as_ref()
485 .unwrap()
486 .blocks
487 .get(&bid)
488 .map_or(-1, |d| d.state);
489
490 let mut ctx = HighlightContext::new(bid, previous_state, user_data);
491 highlighter.highlight_block(text, &mut ctx);
492 let (spans, state, user_data) = ctx.into_parts();
493
494 let hl = self.highlight.as_mut().unwrap();
495 hl.blocks.insert(
496 bid,
497 BlockHighlightData {
498 spans,
499 state,
500 user_data,
501 },
502 );
503
504 if i > start_idx && state == old_state {
507 break;
508 }
509 }
510 }
511
512 pub(crate) fn rehighlight_affected(&mut self, position: usize) {
515 if self.highlight.is_none() {
516 return;
517 }
518
519 let blocks = ordered_block_ids(self);
520
521 let target_bid = blocks
523 .iter()
524 .rev()
525 .find_map(|(id, _)| {
526 let dto = block_commands::get_block(&self.ctx, id).ok().flatten()?;
527 let bp = dto.document_position as usize;
528 if position >= bp {
529 Some(*id as usize)
530 } else {
531 None
532 }
533 })
534 .unwrap_or_else(|| blocks.first().map_or(0, |(id, _)| *id as usize));
535
536 if blocks.is_empty() {
537 return;
538 }
539
540 self.rehighlight_from_block(target_bid);
541 }
542}