Skip to main content

ass_renderer/pipeline/text_segmenter/
mod.rs

1//! Text segmentation for handling inline formatting changes
2
3mod apply;
4mod split;
5
6use apply::process_tag_block;
7
8use crate::utils::RenderError;
9use ass_core::analysis::events::TextAnalysis;
10use ass_core::ExtensionRegistry;
11
12#[cfg(feature = "nostd")]
13use alloc::{
14    string::{String, ToString},
15    vec,
16    vec::Vec,
17};
18#[cfg(not(feature = "nostd"))]
19use std::{
20    string::{String, ToString},
21    vec::Vec,
22};
23
24/// A text segment with its own formatting
25#[derive(Debug, Clone)]
26pub struct TextSegment {
27    /// The plain text content
28    pub text: String,
29    /// Starting position in original text
30    pub start: usize,
31    /// Ending position in original text  
32    pub end: usize,
33    /// Active tags for this segment
34    pub tags: super::tag_processor::ProcessedTags,
35}
36
37/// Process text with inline tag changes into segments
38pub fn segment_text_with_tags(
39    text: &str,
40    _registry: Option<&ExtensionRegistry>,
41) -> Result<Vec<TextSegment>, RenderError> {
42    // Analyze text to get tags and their positions
43    #[cfg(feature = "plugins")]
44    let analysis = TextAnalysis::analyze_with_registry(text, _registry)
45        .map_err(|e| RenderError::InvalidScript(e.to_string()))?;
46
47    #[cfg(not(feature = "plugins"))]
48    let analysis =
49        TextAnalysis::analyze(text).map_err(|e| RenderError::InvalidScript(e.to_string()))?;
50
51    let tags = analysis.override_tags();
52    if tags.is_empty() {
53        // No tags, return single segment with plain text
54        return Ok(vec![TextSegment {
55            text: analysis.plain_text().to_string(),
56            start: 0,
57            end: text.len(),
58            tags: super::tag_processor::ProcessedTags::default(),
59        }]);
60    }
61
62    // Build segments based on tag positions
63    let mut segments = Vec::new();
64    let mut current_tags = super::tag_processor::ProcessedTags::default();
65    let mut last_pos = 0;
66    let mut current_text = String::new();
67
68    // Process text character by character, tracking tag blocks
69    let mut chars = text.chars();
70    let mut pos = 0;
71    let mut in_tag = false;
72    let mut tag_start = 0;
73    let mut brace_depth = 0;
74
75    while let Some(ch) = chars.next() {
76        if ch == '{' {
77            if !in_tag {
78                // Start of tag block
79                if !current_text.is_empty() {
80                    // Save current segment
81                    segments.push(TextSegment {
82                        text: current_text.clone(),
83                        start: last_pos,
84                        end: pos,
85                        tags: current_tags.clone(),
86                    });
87                    current_text.clear();
88                    last_pos = pos;
89                }
90                in_tag = true;
91                tag_start = pos;
92                brace_depth = 1;
93            } else {
94                brace_depth += 1;
95            }
96        } else if ch == '}' && in_tag {
97            brace_depth -= 1;
98            if brace_depth == 0 {
99                // End of tag block - process tags in this block
100                let tag_content = &text[tag_start + 1..pos];
101
102                // Check if this block contains karaoke tags
103                let has_karaoke = tag_content.contains("\\k") || tag_content.contains("\\K");
104
105                if has_karaoke && !current_text.is_empty() {
106                    // If we have karaoke and accumulated text, save it as a segment first
107                    segments.push(TextSegment {
108                        text: current_text.clone(),
109                        start: last_pos,
110                        end: tag_start,
111                        tags: current_tags.clone(),
112                    });
113                    current_text.clear();
114                }
115
116                process_tag_block(tag_content, &mut current_tags)?;
117                in_tag = false;
118                last_pos = pos + 1;
119            }
120        } else if !in_tag {
121            // Regular text
122            if ch == '\\' {
123                // Check for escape sequences
124                if let Some(next) = chars.next() {
125                    pos += next.len_utf8();
126                    match next {
127                        'N' | 'n' => current_text.push('\n'),
128                        'h' => current_text.push('\u{00A0}'),
129                        _ => {
130                            current_text.push(ch);
131                            current_text.push(next);
132                        }
133                    }
134                } else {
135                    current_text.push(ch);
136                }
137            } else {
138                current_text.push(ch);
139            }
140        }
141
142        pos += ch.len_utf8();
143    }
144
145    // Add final segment if there's remaining text
146    if !current_text.is_empty() || segments.is_empty() {
147        segments.push(TextSegment {
148            text: current_text,
149            start: last_pos,
150            end: text.len(),
151            tags: current_tags,
152        });
153    }
154
155    Ok(segments)
156}
157
158// Re-export helper functions from tag_processor
159pub use super::tag_processor::{parse_alpha, parse_color, parse_move_args, parse_pos_args};