ass_renderer/pipeline/text_segmenter/
mod.rs1mod 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#[derive(Debug, Clone)]
26pub struct TextSegment {
27 pub text: String,
29 pub start: usize,
31 pub end: usize,
33 pub tags: super::tag_processor::ProcessedTags,
35}
36
37pub fn segment_text_with_tags(
39 text: &str,
40 _registry: Option<&ExtensionRegistry>,
41) -> Result<Vec<TextSegment>, RenderError> {
42 #[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 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 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 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 if !current_text.is_empty() {
80 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 let tag_content = &text[tag_start + 1..pos];
101
102 let has_karaoke = tag_content.contains("\\k") || tag_content.contains("\\K");
104
105 if has_karaoke && !current_text.is_empty() {
106 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 if ch == '\\' {
123 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 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
158pub use super::tag_processor::{parse_alpha, parse_color, parse_move_args, parse_pos_args};