-module(glubs@webvtt). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]). -export([to_string/1, parse/1, tokenize/1]). -export_type([item/0, web_vtt/0, token/0, tokenization_error/0]). -type item() :: {note, binary()} | {style, binary()} | {cue, gleam@option:option(binary()), integer(), integer(), binary(), list({binary(), binary()})}. -type web_vtt() :: {web_vtt, list({binary(), binary()}), gleam@option:option(binary()), list(item())}. -type token() :: {start_tag, binary(), list(binary()), gleam@option:option(binary())} | {text, binary()} | {timestamp, integer()} | {end_tag, binary()}. -type tokenization_error() :: invalid_start_token | invalid_end_token. -spec header_to_string(gleam@option:option(binary())) -> gleam@string_builder:string_builder(). header_to_string(Comment) -> case Comment of {some, Comment@1} -> gleam@string_builder:from_strings([<<"WEBVTT "/utf8>>, Comment@1]); none -> gleam@string_builder:from_string(<<"WEBVTT"/utf8>>) end. -spec metadata_to_string(list({binary(), binary()})) -> gleam@string_builder:string_builder(). metadata_to_string(Metadata) -> case gleam@list:is_empty(Metadata) of true -> gleam@string_builder:new(); false -> _pipe = Metadata, _pipe@1 = gleam@list:map( _pipe, fun(Item) -> Separator = case erlang:element(1, Item) =:= <<"X-TIMESTAMP-MAP"/utf8>> of true -> <<"="/utf8>>; false -> <<": "/utf8>> end, gleam@string_builder:from_strings( [erlang:element(1, Item), Separator, erlang:element(2, Item)] ) end ), gleam@string_builder:join(_pipe@1, <<"\n"/utf8>>) end. -spec settings_to_string(list({binary(), binary()})) -> gleam@string_builder:string_builder(). settings_to_string(Settings) -> case Settings of [] -> gleam@string_builder:new(); Settings@1 -> _pipe = Settings@1, _pipe@1 = gleam@list:map( _pipe, fun(Item) -> gleam@string_builder:from_strings( [erlang:element(1, Item), <<":"/utf8>>, erlang:element(2, Item)] ) end ), _pipe@2 = gleam@string_builder:join(_pipe@1, <<" "/utf8>>), gleam@string_builder:prepend(_pipe@2, <<" "/utf8>>) end. -spec item_to_string(item()) -> gleam@string_builder:string_builder(). item_to_string(Item) -> case Item of {note, Content} -> case gleam_stdlib:contains_string(Content, <<"\n"/utf8>>) of true -> gleam@string_builder:from_strings( [<<"NOTE\n"/utf8>>, Content] ); false -> gleam@string_builder:from_strings( [<<"NOTE "/utf8>>, Content] ) end; {style, Content@1} -> gleam@string_builder:from_strings([<<"STYLE\n"/utf8>>, Content@1]); {cue, Id, Start_time, End_time, Payload, Settings} -> Start_time@1 = glubs@internal@timestamp:to_string( Start_time, <<"."/utf8>> ), End_time@1 = glubs@internal@timestamp:to_string( End_time, <<"."/utf8>> ), Settings_builder = settings_to_string(Settings), Timestamp = begin _pipe = Start_time@1, _pipe@1 = gleam@string_builder:append(_pipe, <<" --> "/utf8>>), _pipe@2 = gleam@string_builder:append_builder( _pipe@1, End_time@1 ), gleam@string_builder:append_builder(_pipe@2, Settings_builder) end, _pipe@5 = case Id of {some, Id@1} -> _pipe@3 = gleam@string_builder:from_string(Id@1), _pipe@4 = gleam@string_builder:append( _pipe@3, <<"\n"/utf8>> ), gleam@string_builder:append_builder(_pipe@4, Timestamp); none -> Timestamp end, _pipe@6 = gleam@string_builder:append(_pipe@5, <<"\n"/utf8>>), gleam@string_builder:append(_pipe@6, Payload) end. -spec items_to_string(list(item())) -> gleam@string_builder:string_builder(). items_to_string(Items) -> case gleam@list:is_empty(Items) of true -> gleam@string_builder:new(); false -> _pipe = Items, _pipe@1 = gleam@list:map(_pipe, fun item_to_string/1), _pipe@2 = gleam@string_builder:join(_pipe@1, <<"\n\n"/utf8>>), gleam@string_builder:prepend(_pipe@2, <<"\n"/utf8>>) end. -spec to_string(web_vtt()) -> binary(). to_string(Webvtt) -> {web_vtt, Metadata, Comment, Items} = case Webvtt of {web_vtt, _, _, _} -> Webvtt; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Assertion pattern match failed"/utf8>>, value => _assert_fail, module => <<"glubs/webvtt"/utf8>>, function => <<"to_string"/utf8>>, line => 49}) end, _pipe = [header_to_string(Comment), metadata_to_string(Metadata), items_to_string(Items)], _pipe@1 = gleam@list:filter( _pipe, fun(B) -> gleam@string_builder:is_empty(B) =:= false end ), _pipe@2 = gleam@string_builder:join(_pipe@1, <<"\n"/utf8>>), _pipe@3 = gleam@string_builder:append(_pipe@2, <<"\n"/utf8>>), gleam@string_builder:to_string(_pipe@3). -spec parse_metadata(list(binary())) -> {ok, list({binary(), binary()})} | {error, binary()}. parse_metadata(Metadata) -> _pipe = Metadata, gleam@list:try_map(_pipe, fun(Meta) -> case Meta of <<"X-TIMESTAMP-MAP="/utf8, Header/binary>> -> {ok, {<<"X-TIMESTAMP-MAP"/utf8>>, Header}}; _ -> case gleam@string:split_once(Meta, <<": "/utf8>>) of {ok, Entry} -> {ok, Entry}; {error, nil} -> {error, <<"Invalid metadata item"/utf8>>} end end end). -spec parse_comment(binary()) -> {ok, gleam@option:option(binary())} | {error, binary()}. parse_comment(Header) -> case Header of <<"WEBVTT"/utf8>> -> {ok, none}; <<"WEBVTT\t"/utf8, Comment/binary>> -> {ok, {some, Comment}}; <<"WEBVTT "/utf8, Comment@1/binary>> -> {ok, {some, Comment@1}}; <<"WEBVTT"/utf8, _/binary>> -> {error, <<"Header comment must start with space or tab"/utf8>>}; _ -> {error, <<"Must start with \"WEBVTT\""/utf8>>} end. -spec parse_note(binary()) -> {ok, item()} | {error, binary()}. parse_note(Note) -> case Note of <<"NOTE\n"/utf8, Note@1/binary>> -> {ok, {note, Note@1}}; <<"NOTE "/utf8, Note@2/binary>> -> {ok, {note, Note@2}}; _ -> {error, <<"Invalid note"/utf8>>} end. -spec parse_style(binary()) -> {ok, item()} | {error, binary()}. parse_style(Style) -> case Style of <<"STYLE\n"/utf8, Style@1/binary>> -> {ok, {style, Style@1}}; _ -> {error, <<"Invalid style"/utf8>>} end. -spec parse_settings(binary()) -> {ok, list({binary(), binary()})} | {error, binary()}. parse_settings(Settings) -> case Settings /= <<""/utf8>> of true -> _pipe = Settings, _pipe@1 = gleam@string:split(_pipe, <<" "/utf8>>), gleam@list:try_map( _pipe@1, fun(Setting) -> case gleam@string:split_once(Setting, <<":"/utf8>>) of {ok, Item} -> {ok, Item}; {error, nil} -> {error, <<"Invalid cue settings"/utf8>>} end end ); false -> {ok, []} end. -spec parse_cue_id(binary()) -> {ok, {gleam@option:option(binary()), binary()}} | {error, binary()}. parse_cue_id(Cue) -> case gleam@string:split_once(Cue, <<"\n"/utf8>>) of {ok, {Id, Rest}} -> case gleam_stdlib:contains_string(Id, <<"-->"/utf8>>) of true -> {ok, {none, Cue}}; false -> {ok, {{some, Id}, Rest}} end; {error, nil} -> {error, <<"Invalid cue"/utf8>>} end. -spec parse_cue(binary()) -> {ok, item()} | {error, binary()}. parse_cue(Cue) -> gleam@result:'try'( parse_cue_id(Cue), fun(_use0) -> {Id, Rest} = _use0, case gleam@string:split_once(Rest, <<"\n"/utf8>>) of {ok, {Line, Payload}} -> gleam@result:'try'( glubs@internal@timestamp:parse_range(Line, <<"."/utf8>>), fun(_use0@1) -> {Start, End, Rest@1} = _use0@1, gleam@result:'try'( parse_settings(Rest@1), fun(Settings) -> {ok, {cue, Id, Start, End, Payload, Settings}} end ) end ); {error, nil} -> {error, <<"Invalid cue"/utf8>>} end end ). -spec parse_item(binary()) -> {ok, item()} | {error, binary()}. parse_item(Item) -> _pipe = Item, _pipe@1 = parse_note(_pipe), _pipe@2 = gleam@result:try_recover(_pipe@1, fun(_) -> parse_style(Item) end), gleam@result:try_recover(_pipe@2, fun(_) -> parse_cue(Item) end). -spec parse(binary()) -> {ok, web_vtt()} | {error, binary()}. parse(Webvtt) -> _assert_subject = begin _pipe = Webvtt, _pipe@1 = gleam@string:replace(_pipe, <<"\r\n"/utf8>>, <<"\n"/utf8>>), _pipe@2 = gleam@string:trim_right(_pipe@1), gleam@string:split(_pipe@2, <<"\n\n"/utf8>>) end, [Header | Body] = case _assert_subject of [_ | _] -> _assert_subject; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Assertion pattern match failed"/utf8>>, value => _assert_fail, module => <<"glubs/webvtt"/utf8>>, function => <<"parse"/utf8>>, line => 32}) end, _assert_subject@1 = gleam@string:split(Header, <<"\n"/utf8>>), [Header@1 | Metadata] = case _assert_subject@1 of [_ | _] -> _assert_subject@1; _assert_fail@1 -> erlang:error(#{gleam_error => let_assert, message => <<"Assertion pattern match failed"/utf8>>, value => _assert_fail@1, module => <<"glubs/webvtt"/utf8>>, function => <<"parse"/utf8>>, line => 38}) end, gleam@result:'try'( parse_comment(Header@1), fun(Comment) -> gleam@result:'try'( parse_metadata(Metadata), fun(Metadata@1) -> gleam@result:'try'( gleam@list:try_map(Body, fun parse_item/1), fun(Items) -> {ok, {web_vtt, Metadata@1, Comment, Items}} end ) end ) end ). -spec parse_tag_and_classes(binary()) -> {binary(), list(binary())}. parse_tag_and_classes(Input) -> _assert_subject = gleam@string:split(Input, <<"."/utf8>>), [Tag | Classes] = case _assert_subject of [_ | _] -> _assert_subject; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Assertion pattern match failed"/utf8>>, value => _assert_fail, module => <<"glubs/webvtt"/utf8>>, function => <<"parse_tag_and_classes"/utf8>>, line => 320}) end, {Tag, Classes}. -spec parse_start_tag(binary()) -> token(). parse_start_tag(Input) -> case gleam@string:split_once(Input, <<" "/utf8>>) of {ok, {Tag_and_classes, Annotation}} -> {Tag, Classes} = parse_tag_and_classes(Tag_and_classes), {start_tag, Tag, Classes, {some, Annotation}}; {error, _} -> {Tag@1, Classes@1} = parse_tag_and_classes(Input), {start_tag, Tag@1, Classes@1, none} end. -spec do_tokenize(binary(), list(token())) -> {ok, list(token())} | {error, tokenization_error()}. do_tokenize(Payload, Acc) -> case Payload of <<""/utf8>> -> {ok, Acc}; <<"> -> case gleam@string:split_once(Rest, <<">"/utf8>>) of {ok, {Tag, Rest@1}} -> do_tokenize(Rest@1, [{end_tag, Tag} | Acc]); {error, nil} -> {error, invalid_end_token} end; <<"<"/utf8, Rest@2/binary>> -> case gleam@string:split_once(Rest@2, <<">"/utf8>>) of {ok, {Tag@1, Rest@3}} -> case glubs@internal@timestamp:parse(Tag@1, <<"."/utf8>>) of {ok, Ts} -> do_tokenize(Rest@3, [{timestamp, Ts} | Acc]); {error, _} -> do_tokenize(Rest@3, [parse_start_tag(Tag@1) | Acc]) end; {error, nil} -> {error, invalid_start_token} end; Text -> case gleam@string:split_once(Text, <<"<"/utf8>>) of {ok, {Content, Rest@4}} -> do_tokenize( <<"<"/utf8, Rest@4/binary>>, [{text, Content} | Acc] ); {error, nil} -> {ok, [{text, Text} | Acc]} end end. -spec tokenize(binary()) -> {ok, list(token())} | {error, tokenization_error()}. tokenize(Payload) -> _pipe = Payload, _pipe@1 = do_tokenize(_pipe, []), gleam@result:map(_pipe@1, fun gleam@list:reverse/1).