Skip to content

Commit

Permalink
Add support for STYLE and cue settings
Browse files Browse the repository at this point in the history
  • Loading branch information
philipgiuliani committed Nov 19, 2023
1 parent 2a5cab2 commit 724d8e0
Show file tree
Hide file tree
Showing 8 changed files with 189 additions and 14 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ and its documentation can be found at <https://hexdocs.pm/glubs>.
* [x] Converts a WebVTT type back to a string
* [x] Parse SRT
* [x] Convert SRT to string
* [ ] Support WebVTT STYLE-Tags (https://developer.mozilla.org/en-US/docs/Web/API/WebVTT_API#styling_webvtt_cues)
* [ ] Support WebVTT cue settings (https://developer.mozilla.org/en-US/docs/Web/API/WebVTT_API#cue_settings)
* [x] Support WebVTT STYLE-Tags (https://developer.mozilla.org/en-US/docs/Web/API/WebVTT_API#styling_webvtt_cues)
* [x] Support WebVTT cue settings (https://developer.mozilla.org/en-US/docs/Web/API/WebVTT_API#cue_settings)
* [ ] Support WebVTT header metadata

## Example
Expand Down
2 changes: 1 addition & 1 deletion src/glubs/srt.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ fn parse_cue(input: String) -> Result(Cue, String) {
|> result.replace_error("Cannot parse identifier"),
)

use #(start_time, end_time) <- result.try(timestamp.parse_range(ts, ","))
use #(start_time, end_time, "") <- result.try(timestamp.parse_range(ts, ","))

Ok(Cue(
id: id,
Expand Down
11 changes: 8 additions & 3 deletions src/glubs/timestamp.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,27 @@ pub fn parse(input: String, fraction_sep: String) -> Result(Int, Nil) {
pub fn parse_range(
line: String,
fraction_sep: String,
) -> Result(#(Int, Int), String) {
) -> Result(#(Int, Int, String), String) {
case string.split(line, " --> ") {
[start, end] -> {
[start, end_with_rest] -> {
use start <- result.try(
start
|> parse(fraction_sep)
|> result.replace_error("Invalid start timestamp"),
)

let #(end, rest) = case string.split_once(end_with_rest, " ") {
Ok(result) -> result
Error(Nil) -> #(end_with_rest, "")
}

use end <- result.try(
end
|> parse(fraction_sep)
|> result.replace_error("Invalid end timestamp"),
)

Ok(#(start, end))
Ok(#(start, end, rest))
}
_other -> Error("Invalid timestamp")
}
Expand Down
68 changes: 64 additions & 4 deletions src/glubs/webvtt.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@ import glubs/timestamp
/// Item represents an individual item in a WebVTT file, which can be either a Note or a Cue.
pub type Item {
Note(String)
Cue(id: Option(String), start_time: Int, end_time: Int, payload: String)
Style(String)
Cue(
id: Option(String),
start_time: Int,
end_time: Int,
payload: String,
settings: List(#(String, String)),
)
}

/// Represents a WebVTT file with an optional comment and a list of items.
Expand Down Expand Up @@ -66,13 +73,22 @@ fn item_to_string(item: Item) -> StringBuilder {
True -> string_builder.from_strings(["NOTE\n", content])
False -> string_builder.from_strings(["NOTE ", content])
}
Cue(id: id, start_time: start_time, end_time: end_time, payload: payload) -> {
Style(content) -> string_builder.from_strings(["STYLE\n", content])
Cue(
id: id,
start_time: start_time,
end_time: end_time,
payload: payload,
settings: settings,
) -> {
let start_time = timestamp.to_string(start_time, ".")
let end_time = timestamp.to_string(end_time, ".")
let settings_builder = settings_to_string(settings)
let timestamp =
start_time
|> string_builder.append(" --> ")
|> string_builder.append_builder(end_time)
|> string_builder.append_builder(settings_builder)

case id {
Some(id) -> {
Expand All @@ -88,6 +104,19 @@ fn item_to_string(item: Item) -> StringBuilder {
}
}

fn settings_to_string(settings: List(#(String, String))) -> StringBuilder {
case settings {
[] -> string_builder.new()
settings ->
settings
|> list.map(fn(item) {
string_builder.from_strings([item.0, ":", item.1])
})
|> string_builder.join(" ")
|> string_builder.prepend(" ")
}
}

fn parse_comment(header: String) -> Result(Option(String), String) {
case header {
"WEBVTT" -> Ok(None)
Expand All @@ -101,6 +130,7 @@ fn parse_comment(header: String) -> Result(Option(String), String) {
fn parse_item(item: String) -> Result(Item, String) {
item
|> parse_note()
|> result.try_recover(fn(_) { parse_style(item) })
|> result.try_recover(fn(_) { parse_cue(item) })
}

Expand All @@ -112,18 +142,48 @@ fn parse_note(note: String) -> Result(Item, String) {
}
}

fn parse_style(style: String) -> Result(Item, String) {
case style {
"STYLE\n" <> style -> Ok(Style(style))
_other -> Error("Invalid style")
}
}

fn parse_cue(cue: String) -> Result(Item, String) {
use #(id, rest) <- result.try(parse_cue_id(cue))

case string.split_once(rest, "\n") {
Ok(#(line, payload)) -> {
use #(start, end) <- result.try(timestamp.parse_range(line, "."))
Ok(Cue(id: id, payload: payload, start_time: start, end_time: end))
use #(start, end, rest) <- result.try(timestamp.parse_range(line, "."))
use settings <- result.try(parse_settings(rest))

Ok(Cue(
id: id,
payload: payload,
start_time: start,
end_time: end,
settings: settings,
))
}
Error(Nil) -> Error("Invalid cue")
}
}

fn parse_settings(settings: String) -> Result(List(#(String, String)), String) {
case settings != "" {
True ->
settings
|> string.split(" ")
|> list.try_map(fn(setting) {
case string.split_once(setting, ":") {
Ok(item) -> Ok(item)
Error(Nil) -> Error("Invalid cue settings")
}
})
False -> Ok([])
}
}

fn parse_cue_id(cue: String) -> Result(#(Option(String), String), String) {
case string.split_once(cue, "\n") {
Ok(#(id, rest)) -> {
Expand Down
File renamed without changes.
10 changes: 10 additions & 0 deletions test/fixtures/webvtt/settings.vtt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
WEBVTT
00:00:00.000 --> 00:00:04.000 position:10%,line-left align:left size:35%
Where did he go?

00:00:03.000 --> 00:00:06.500 position:90% align:right size:35%
I think he went down this lane.

00:00:04.000 --> 00:00:06.500 position:45%,line-right align:center size:35%
What are you waiting for?
19 changes: 19 additions & 0 deletions test/fixtures/webvtt/style.vtt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
WEBVTT
STYLE
::cue {
background-image: linear-gradient(to bottom, dimgray, lightgray);
color: papayawhip;
}
NOTE comment blocks can be used between style blocks.
STYLE
::cue(b) {
color: peachpuff;
}
00:00:00.000 --> 00:00:10.000
- Hello <b>world</b>.

NOTE style blocks cannot appear after the first cue.
89 changes: 85 additions & 4 deletions test/glubs/webvtt_test.gleam
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import gleeunit/should
import gleam/option.{None, Some}
import glubs/webvtt.{Cue, EndTag, Note, StartTag, Text, Timestamp, WebVTT}
import glubs/webvtt.{Cue,
EndTag, Note, StartTag, Style, Text, Timestamp, WebVTT}
import simplifile

pub fn parse_invalid_header_test() {
Expand Down Expand Up @@ -32,12 +33,20 @@ pub fn parse_cue_test() {
|> webvtt.parse()
|> should.equal(Ok(WebVTT(
comment: None,
items: [Cue(id: Some("1"), start_time: 123, end_time: 456, payload: "Test")],
items: [
Cue(
id: Some("1"),
start_time: 123,
end_time: 456,
payload: "Test",
settings: [],
),
],
)))
}

pub fn parse_comment_test() {
let assert Ok(content) = simplifile.read("test/fixtures/comments.vtt")
let assert Ok(content) = simplifile.read("test/fixtures/webvtt/comments.vtt")

content
|> webvtt.parse()
Expand All @@ -52,26 +61,95 @@ pub fn parse_comment_test() {
start_time: 135_000,
end_time: 140_000,
payload: "- Ta en kopp varmt te.\n- Det är inte varmt.",
settings: [],
),
Cue(
id: Some("2"),
start_time: 140_000,
end_time: 145_000,
payload: "- Har en kopp te.\n- Det smakar som te.",
settings: [],
),
Note("This last line may not translate well."),
Cue(
id: Some("3"),
start_time: 145_000,
end_time: 150_000,
payload: "- Ta en kopp",
settings: [],
),
],
)))
}

pub fn parse_style_test() {
let assert Ok(content) = simplifile.read("test/fixtures/webvtt/style.vtt")

content
|> webvtt.parse()
|> should.equal(Ok(WebVTT(
comment: None,
items: [
Style(
"::cue {\n background-image: linear-gradient(to bottom, dimgray, lightgray);\n color: papayawhip;\n}",
),
Note("comment blocks can be used between style blocks."),
Style("::cue(b) {\n color: peachpuff;\n}"),
Cue(
id: None,
start_time: 0,
end_time: 10_000,
payload: "- Hello <b>world</b>.",
settings: [],
),
Note("style blocks cannot appear after the first cue."),
],
)))
}

pub fn parse_settings_test() {
let assert Ok(content) = simplifile.read("test/fixtures/webvtt/settings.vtt")

content
|> webvtt.parse()
|> should.equal(Ok(WebVTT(
comment: None,
items: [
Cue(
id: None,
start_time: 0,
end_time: 4000,
payload: "Where did he go?",
settings: [
#("position", "10%,line-left"),
#("align", "left"),
#("size", "35%"),
],
),
Cue(
id: None,
start_time: 3000,
end_time: 6500,
payload: "I think he went down this lane.",
settings: [#("position", "90%"), #("align", "right"), #("size", "35%")],
),
Cue(
id: None,
start_time: 4000,
end_time: 6500,
payload: "What are you waiting for?",
settings: [
#("position", "45%,line-right"),
#("align", "center"),
#("size", "35%"),
],
),
],
)))
}

pub fn to_string_test() {
let assert Ok(expected) = simplifile.read("test/fixtures/comments.vtt")
let assert Ok(expected) = simplifile.read("test/fixtures/webvtt/comments.vtt")

WebVTT(
comment: Some("- Translation of that film I like"),
Expand All @@ -84,19 +162,22 @@ pub fn to_string_test() {
start_time: 135_000,
end_time: 140_000,
payload: "- Ta en kopp varmt te.\n- Det är inte varmt.",
settings: [],
),
Cue(
id: Some("2"),
start_time: 140_000,
end_time: 145_000,
payload: "- Har en kopp te.\n- Det smakar som te.",
settings: [],
),
Note("This last line may not translate well."),
Cue(
id: Some("3"),
start_time: 145_000,
end_time: 150_000,
payload: "- Ta en kopp",
settings: [],
),
],
)
Expand Down

0 comments on commit 724d8e0

Please sign in to comment.