Skip to content

Commit

Permalink
First commit
Browse files Browse the repository at this point in the history
  • Loading branch information
mattmatters committed May 29, 2019
0 parents commit 90f1e36
Show file tree
Hide file tree
Showing 13 changed files with 493 additions and 0 deletions.
40 changes: 40 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
version: 2
jobs:
build:
parallelism: 1
docker:
- image: circleci/elixir:1.8.0
environment:
MIX_ENV: test
working_directory: ~/app
steps:
- checkout
- run: mix local.hex --force
- run: mix local.rebar --force
- restore_cache:
keys:
- v1-mix-cache-{{ .Branch }}-{{ checksum "mix.lock" }}
- v1-mix-cache-{{ .Branch }}
- v1-mix-cache
- restore_cache:
keys:
- v1-build-cache-{{ .Branch }}
- v1-build-cache
- run: mix do deps.get, compile
- save_cache:
key: v1-mix-cache-{{ .Branch }}-{{ checksum "mix.lock" }}
paths: "deps"
- save_cache:
key: v1-mix-cache-{{ .Branch }}
paths: "deps"
- save_cache:
key: v1-mix-cache
paths: "deps"
- save_cache:
key: v1-build-cache-{{ .Branch }}
paths: "_build"
- save_cache:
key: v1-build-cache
paths: "_build"
- run: mix format --check-formatted
- run: mix test
4 changes: 4 additions & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
24 changes: 24 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where third-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Ignore package tarball (built via "mix hex.build").
vttyl-*.tar

46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Vttyl

> A dead simple vtt parser in Elixir.
## Installation

To install Vttyl, add it to your `mix.exs` file.

```elixir
def deps do
[
{:vttyl, "~> 0.1.0"}
]
end
```

Then, run `$ mix deps.get`.

## Usage

Vttyl has two basic ways to use it.

### String Parsing

```elixir
iex> vtt = """
WEBVTT
1
00:00:15.450 --> 00:00:17.609
Hello world!
"""
...> Vttyl.parse(vtt)
[%Vttyl.Part{end: ~T[00:00:17.609], part: 1, start: ~T[00:00:15.450], text: "Hello world!"}]
```

### Stream Parsing

```elixir
iex> "same_text.vtt" |> File.stream!([], 2048) |> Vttyl.parse_stream() |> Enum.into([])
[%Vttyl.Part{end: ~T[00:00:17.609], part: 1, start: ~T[00:00:15.450], text: "Hello world!"}]
```

For more information, see [the documentation][documentation].

[documentation]: https://hexdocs.pm/vttyl
30 changes: 30 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
use Mix.Config

# This configuration is loaded before any dependency and is restricted
# to this project. If another project depends on this project, this
# file won't be loaded nor affect the parent project. For this reason,
# if you want to provide default values for your application for
# third-party users, it should be done in your "mix.exs" file.

# You can configure your application as:
#
# config :vttyl, key: :value
#
# and access this configuration in your application as:
#
# Application.get_env(:vttyl, :key)
#
# You can also configure a third-party app:
#
# config :logger, level: :info
#

# It is also possible to import configuration files, relative to this
# directory. For example, you can emulate configuration per environment
# by uncommenting the line below and defining dev.exs, test.exs and such.
# Configuration from the imported file will override the ones defined
# here (which is why it is important to import them last).
#
# import_config "#{Mix.env()}.exs"
102 changes: 102 additions & 0 deletions lib/vttyl.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
defmodule Vttyl do
@moduledoc """
Quick and dirty conveniences for working with utf8 encoded vtt files.
"""

alias Vttyl.Part

@doc """
Parse a string.
This drops badly formatted vtt files. Consult your local doctor to ensure you formatted it correctly.
"""
@spec parse(String.t()) :: [Part.t()]
def parse(content) do
content
|> String.splitter("\n")
|> do_parse()
|> Enum.into([])
end

@doc """
Parse a stream of utf8 encoded characters.
This returns a stream so you decide how to handle it!
"""
@spec parse_stream(Enumerable.t()) :: Enumerable.t()
def parse_stream(content) do
content
|> Stream.transform("", &next_line/2)
|> do_parse()
end

defp do_parse(enum_content) do
enum_content
|> Stream.map(fn line -> Regex.replace(~r/#.*/, line, "") end)
|> Stream.map(&String.trim/1)
|> Stream.reject(&(&1 in ["", "WEBVTT"]))
|> Stream.chunk_while(%Part{}, &parse_chunk/2, &parse_chunk_after/1)
|> Stream.filter(&full_chunk?/1)
end

defp parse_chunk(line, acc) do
acc =
cond do
Regex.match?(~r/^\d+$/, line) ->
%Part{acc | part: String.to_integer(line)}

not is_nil(acc.part) and timestamps?(line) ->
{start_ts, end_ts} = parse_timestamps(line)

%Part{acc | start: start_ts, end: end_ts}

# Text content should be on one line and the other stuff should have appeared
not is_nil(acc.part) and not is_nil(acc.start) and not is_nil(acc.end) and line != "" ->
%Part{acc | text: line}

true ->
acc
end

if full_chunk?(acc) do
{:cont, acc, %Part{}}
else
{:cont, acc}
end
end

defp parse_chunk_after(acc) do
if full_chunk?(acc) do
{:cont, acc, %Part{}}
else
{:cont, acc}
end
end

defp full_chunk?(%Part{part: part, start: start, end: ts_end, text: text}) do
not is_nil(part) and not is_nil(start) and not is_nil(ts_end) and not is_nil(text)
end

# 00:00:00.000 --> 00:01:01.000
defp timestamps?(line) do
Regex.match?(~r/(\d{2}:)?\d{2}:\d{2}\.\d{3} --> (\d{2}:)?\d{2}:\d{2}.\d{3}/, line)
end

defp parse_timestamps(line) do
line
|> String.split("-->")
|> Enum.map(fn ts -> ts |> String.trim() |> Time.from_iso8601!() end)
|> List.to_tuple()
end

defp next_line(chunk, acc) do
case String.split(<<acc::binary, chunk::binary>>, "\n") do
[] ->
{[], ""}

lines ->
{acc, lines} = List.pop_at(lines, -1)
{lines, acc}
end
end
end
9 changes: 9 additions & 0 deletions lib/vttyl/part.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
defmodule Vttyl.Part do
@type t :: %__MODULE__{
part: non_neg_integer(),
start: Time.t(),
end: Time.t(),
text: String.t()
}
defstruct [:start, :end, :text, :part]
end
47 changes: 47 additions & 0 deletions mix.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
defmodule Vttyl.MixProject do
use Mix.Project

@version "0.1.0"
@repo_url "https://github.com/grain-team/vttyl"

def project do
[
app: :vttyl,
version: @version,
elixir: "~> 1.8",
start_permanent: Mix.env() == :prod,
deps: deps(),

# Hex
package: package(),
description: "A dead simple vtt parser.",

# Docs
name: "Vttyl",
docs: [
source_ref: "v#{@version}",
source_url: @repo_url
]
]
end

def application do
[
extra_applications: [:logger]
]
end

defp package do
[
licenses: ["MIT"],
links: %{"GitHub" => @repo_url}
]
end

defp deps do
[
{:ex_doc, "~> 0.20", only: :dev},
{:dialyxir, "~> 1.0.0-rc.6", only: [:dev, :test], runtime: false}
]
end
end
9 changes: 9 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
%{
"dialyxir": {:hex, :dialyxir, "1.0.0-rc.6", "78e97d9c0ff1b5521dd68041193891aebebce52fc3b93463c0a6806874557d7d", [:mix], [{:erlex, "~> 0.2.1", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm"},
"earmark": {:hex, :earmark, "1.3.2", "b840562ea3d67795ffbb5bd88940b1bed0ed9fa32834915125ea7d02e35888a5", [:mix], [], "hexpm"},
"erlex": {:hex, :erlex, "0.2.1", "cee02918660807cbba9a7229cae9b42d1c6143b768c781fa6cee1eaf03ad860b", [:mix], [], "hexpm"},
"ex_doc": {:hex, :ex_doc, "0.20.2", "1bd0dfb0304bade58beb77f20f21ee3558cc3c753743ae0ddbb0fd7ba2912331", [:mix], [{:earmark, "~> 1.3", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.10", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"},
"makeup": {:hex, :makeup, "0.8.0", "9cf32aea71c7fe0a4b2e9246c2c4978f9070257e5c9ce6d4a28ec450a839b55f", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},
"makeup_elixir": {:hex, :makeup_elixir, "0.13.0", "be7a477997dcac2e48a9d695ec730b2d22418292675c75aa2d34ba0909dcdeda", [:mix], [{:makeup, "~> 0.8", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"},
"nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm"},
}
Loading

0 comments on commit 90f1e36

Please sign in to comment.