// SRT / WebVTT 解析。
//
// 解析是纯函数:失败时返回带行号的错误信息,不抛异常。
///|
fn chars_to_string(chars : Array[Char], start : Int, end : Int) -> String {
let buffer = StringBuilder()
let mut i = start
while i < end && i < chars.length() {
buffer.write_char(chars[i])
i = i + 1
}
buffer.to_string()
}
///|
fn split_char(text : String, separator : Char) -> Array[String] {
let parts : Array[String] = []
let buffer = StringBuilder()
for c in text {
if c == separator {
parts.push(buffer.to_string())
buffer.reset()
} else {
buffer.write_char(c)
}
}
parts.push(buffer.to_string())
parts
}
///|
fn lines_of(text : String) -> Array[String] {
let source = if text.has_prefix("\u{feff}") {
chars_to_string(text.iter().to_array(), 1, text.length())
} else {
text
}
let lines : Array[String] = []
let buffer = StringBuilder()
for c in source {
if c == '\n' {
lines.push(buffer.to_string())
buffer.reset()
} else if c != '\r' {
buffer.write_char(c)
}
}
if !source.has_suffix("\n") {
lines.push(buffer.to_string())
}
lines
}
///|
fn parse_uint(raw : String) -> Int? {
if raw.is_empty() {
return None
}
let mut value = 0
for c in raw {
if !c.is_ascii_digit() {
return None
}
value = value * 10 + (c.to_int() - 48)
}
Some(value)
}
///|
fn parse_timestamp(raw : String) -> Int? {
let parts = split_char(raw.trim().to_owned(), ':')
let count = parts.length()
if count < 2 || count > 3 {
return None
}
let hours = if count == 3 {
match parse_uint(parts[0]) {
Some(value) => value
None => return None
}
} else {
0
}
let minutes_raw = if count == 3 { parts[1] } else { parts[0] }
let minutes = match parse_uint(minutes_raw) {
Some(value) => value
None => return None
}
let seconds_raw = if count == 3 { parts[2] } else { parts[1] }
let comma_parts = split_char(seconds_raw, ',')
let seconds_text = comma_parts[0]
let comma_fraction = if comma_parts.length() > 1 {
comma_parts[1]
} else {
""
}
let dot_parts = split_char(seconds_text, '.')
let seconds = match parse_uint(dot_parts[0]) {
Some(value) => value
None => return None
}
let fraction = if dot_parts.length() > 1 {
dot_parts[1]
} else {
comma_fraction
}
let millis = if !fraction.is_empty() {
if fraction.length() > 3 {
return None
}
let padded = if fraction.length() == 1 {
fraction + "00"
} else if fraction.length() == 2 {
fraction + "0"
} else {
fraction
}
match parse_uint(padded) {
Some(value) => value
None => return None
}
} else {
0
}
Some(((hours * 60 + minutes) * 60 + seconds) * 1000 + millis)
}
///|
fn find_substring(text : String, needle : String) -> Int? {
let chars = text.iter().to_array()
let pattern = needle.iter().to_array()
if pattern.length() == 0 || chars.length() < pattern.length() {
return None
}
let mut i = 0
while i + pattern.length() <= chars.length() {
let mut j = 0
let mut matched = true
while j < pattern.length() {
if chars[i + j] != pattern[j] {
matched = false
break
}
j = j + 1
}
if matched {
return Some(i)
}
i = i + 1
}
None
}
///|
fn first_token(text : String) -> String {
let chars = text.iter().to_array()
let mut end = chars.length()
let mut i = 0
while i < chars.length() {
if chars[i] == ' ' || chars[i] == '\t' {
end = i
break
}
i = i + 1
}
chars_to_string(chars, 0, end)
}
///|
fn parse_timestamp_line(line : String) -> (Int, Int)? {
match find_substring(line, "-->") {
None => None
Some(position) => {
let chars = line.iter().to_array()
let left = chars_to_string(chars, 0, position)
let right_start = position + 3
let right = chars_to_string(chars, right_start, chars.length())
let start = match parse_timestamp(left) {
Some(value) => value
None => return None
}
let end = match parse_timestamp(first_token(right.trim().to_owned())) {
Some(value) => value
None => return None
}
Some((start, end))
}
}
}
///|
fn is_blank(line : String) -> Bool {
line.trim().is_empty()
}
///|
fn is_index_line(line : String) -> Bool {
let text = line.trim()
if text.is_empty() {
return false
}
for c in text {
if !c.is_ascii_digit() {
return false
}
}
true
}
///|
/// 解析时间码(供 CLI 解析 `--by` 等参数)。
pub fn parse_timecode(text : String) -> Int? {
parse_timestamp(text)
}
///|
/// 解析 SRT。
pub fn parse_srt(text : String) -> Result[Subtitle, String] {
let lines = lines_of(text)
let cues : Array[Cue] = []
let mut i = 0
while i < lines.length() {
while i < lines.length() && is_blank(lines[i]) {
i = i + 1
}
if i >= lines.length() {
break
}
let line_number = i + 1
let mut index = cues.length() + 1
if is_index_line(lines[i]) {
match parse_uint(lines[i].trim().to_owned()) {
Some(value) => index = value
None => ()
}
i = i + 1
}
if i >= lines.length() {
return Err("第 \{line_number} 行:缺少时间轴")
}
let timing = match parse_timestamp_line(lines[i]) {
Some(value) => value
None =>
return Err("第 \{i + 1} 行:无法解析时间轴「\{lines[i]}」")
}
i = i + 1
let text_lines : Array[String] = []
while i < lines.length() && !is_blank(lines[i]) {
text_lines.push(lines[i])
i = i + 1
}
cues.push({
index,
start_ms: timing.0,
end_ms: timing.1,
text: join_lines(text_lines),
})
}
if cues.length() == 0 {
return Err("没有解析到任何字幕")
}
Ok({ cues, })
}
///|
fn join_lines(lines : Array[String]) -> String {
let buffer = StringBuilder()
let mut i = 0
while i < lines.length() {
if i > 0 {
buffer.write_string("\n")
}
buffer.write_string(lines[i])
i = i + 1
}
buffer.to_string()
}
///|
/// 解析 WebVTT。
pub fn parse_vtt(text : String) -> Result[Subtitle, String] {
let lines = lines_of(text)
if lines.length() == 0 || !lines[0].trim().has_prefix("WEBVTT") {
return Err("缺少 WEBVTT 头")
}
let cues : Array[Cue] = []
let mut i = 1
while i < lines.length() {
while i < lines.length() && is_blank(lines[i]) {
i = i + 1
}
if i >= lines.length() {
break
}
if lines[i].trim().has_prefix("NOTE") ||
lines[i].trim().has_prefix("STYLE") ||
lines[i].trim().has_prefix("REGION") {
while i < lines.length() && !is_blank(lines[i]) {
i = i + 1
}
continue
}
if find_substring(lines[i], "-->") is None {
if i + 1 < lines.length() &&
find_substring(lines[i + 1], "-->") is Some(_) {
i = i + 1
} else {
while i < lines.length() && !is_blank(lines[i]) {
i = i + 1
}
continue
}
}
let timing = match parse_timestamp_line(lines[i]) {
Some(value) => value
None => return Err("第 \{i + 1} 行:无法解析 VTT 时间轴")
}
i = i + 1
let text_lines : Array[String] = []
while i < lines.length() && !is_blank(lines[i]) {
text_lines.push(lines[i])
i = i + 1
}
cues.push({
index: cues.length() + 1,
start_ms: timing.0,
end_ms: timing.1,
text: join_lines(text_lines),
})
}
if cues.length() == 0 {
return Err("没有解析到任何字幕")
}
Ok({ cues, })
}
///|
/// 按格式解析。
pub fn parse_subtitle(
text : String,
format : SubtitleFormat,
) -> Result[Subtitle, String] {
match format {
Srt => parse_srt(text)
Vtt => parse_vtt(text)
}
}