Handle installing Pandoc
This commit is contained in:
parent
73ff333cfc
commit
09ca9b8245
32
LICENCE.md
Normal file
32
LICENCE.md
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# MIT License
|
||||||
|
|
||||||
|
Most of this code is copied directly from
|
||||||
|
[esbuild](https://github.com/phoenixframework/esbuild) and
|
||||||
|
[tailwind](https://github.com/phoenixframework/tailwind), so I guess,
|
||||||
|
technically, some or all of these people own the copyrights:
|
||||||
|
|
||||||
|
Copyright (c) 2021 Wojtek Mach, José Valim. Copyright (c) 2022 Chris McCord.
|
||||||
|
|
||||||
|
And for the bits that I wrote:
|
||||||
|
|
||||||
|
Copyright (c) 2024 Catalin Mititiuc.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the
|
||||||
|
"Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
209
README.md
209
README.md
@ -1,168 +1,117 @@
|
|||||||
# Pandoc
|
# Pandoc
|
||||||
|
|
||||||
A watcher and a Mix task that uses Pandoc to convert markdown files to html.
|
A watcher and Mix tasks for installing and invoking [pandoc](https://pandoc.org/).
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- inotify-tools
|
Currently only supports `linux-amd64` architectures.
|
||||||
- pandoc
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
```elixir
|
If you are going to convert markup in production, then you add `pandoc` as
|
||||||
# mix.exs
|
dependency on all environments but only start it in dev:
|
||||||
|
|
||||||
|
```elixir
|
||||||
def deps do
|
def deps do
|
||||||
[
|
[
|
||||||
{:pandoc, "~> 0.2.0", runtime: Mix.env() == :dev}
|
{:pandoc, "~> 0.3", runtime: Mix.env() == :dev}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
## Use
|
However, if your markup is preconverted during development, then it only needs
|
||||||
|
to be a dev dependency:
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
# mix.exs
|
def deps do
|
||||||
|
|
||||||
# ...
|
|
||||||
defp aliases do
|
|
||||||
[
|
[
|
||||||
# ...
|
{:pandoc, "~> 0.3", only: :dev}
|
||||||
"documents.build": ["pandoc hello"],
|
|
||||||
"statics.build": ["assets.build", "documents.build"],
|
|
||||||
"statics.deploy": ["assets.deploy", "documents.build"]
|
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
```elixir
|
Once installed, change your `config/config.exs` to pick your pandoc version of
|
||||||
# config/config.exs
|
choice:
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
config :pandoc, version: "3.6.1"
|
||||||
|
```
|
||||||
|
|
||||||
|
Now you can install pandoc by running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ mix pandoc.install
|
||||||
|
```
|
||||||
|
|
||||||
|
And invoke pandoc with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ mix pandoc default documents/hello.md -o priv/static/posts/hello.html
|
||||||
|
```
|
||||||
|
|
||||||
|
The executable is kept at `_build/pandoc-TARGET`. Where `TARGET` is your
|
||||||
|
system target architecture.
|
||||||
|
|
||||||
|
## Profiles
|
||||||
|
|
||||||
|
The first argument to `pandoc` is the execution profile. You can define multiple
|
||||||
|
execution profiles with the current directory, the OS environment, and default
|
||||||
|
arguments to the `pandoc` task:
|
||||||
|
|
||||||
|
```elixir
|
||||||
config :pandoc,
|
config :pandoc,
|
||||||
hello: [
|
version: "3.6.1",
|
||||||
args: ~w(--mathjax -o ../priv/static/posts),
|
default: [
|
||||||
|
args: ~w(--mathjax),
|
||||||
cd: Path.expand("../documents", __DIR__)
|
cd: Path.expand("../documents", __DIR__)
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
```elixir
|
When `mix pandoc default` is invoked, the task arguments will be appended to
|
||||||
# config/dev.exs
|
the ones configured above. Note profiles must be configured in your
|
||||||
|
`config/config.exs`, as `pandoc` runs without starting your application (and
|
||||||
|
therefore it won't pick settings in `config/runtime.exs`).
|
||||||
|
|
||||||
config :hello, HelloWeb.Endpoint,
|
## Adding to Phoenix
|
||||||
# ...
|
|
||||||
watchers: [
|
To add `pandoc` to an application using Phoenix, you will need Phoenix v1.6+ and
|
||||||
# ...
|
the following steps.
|
||||||
pandoc: {Pandoc, :run, [:hello, ~w(--watch)]}
|
|
||||||
|
First add it as a dependency in your `mix.exs`:
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
def deps do
|
||||||
|
[
|
||||||
|
{:phoenix, "~> 1.6"},
|
||||||
|
{:pandoc, "~> 0.3", runtime: Mix.env() == :dev}
|
||||||
]
|
]
|
||||||
|
|
||||||
config :pandoc, hello: [pattern: "**/*.md"]
|
|
||||||
```
|
|
||||||
|
|
||||||
```elixir
|
|
||||||
# lib/hello_web/router.ex
|
|
||||||
|
|
||||||
scope "/", HelloWeb do
|
|
||||||
pipe_through :browser
|
|
||||||
|
|
||||||
get "/drafts/:id", PostController, :draft
|
|
||||||
get "/posts/:id", PostController, :show
|
|
||||||
get "/posts", PostController, :index
|
|
||||||
|
|
||||||
get "/", PageController, :home
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
```elixir
|
|
||||||
# lib/hello_web/controllers/posts_controller.ex
|
|
||||||
|
|
||||||
defmodule HelloWeb.PostController do
|
|
||||||
use HelloWeb, :controller
|
|
||||||
|
|
||||||
alias Hello.Document
|
|
||||||
@path "documents/**/*.md"
|
|
||||||
paths = Path.wildcard(@path)
|
|
||||||
@paths_hash :erlang.md5(paths)
|
|
||||||
for path <- paths, do: @external_resource(path)
|
|
||||||
@posts Document.list()
|
|
||||||
|
|
||||||
def __mix_recompile__?(), do: @path |> Path.wildcard() |> :erlang.md5() != @paths_hash
|
|
||||||
|
|
||||||
def index(conn, _params) do
|
|
||||||
render(conn, :index, posts: @posts)
|
|
||||||
end
|
|
||||||
|
|
||||||
def show(conn, %{"id" => id}) do
|
|
||||||
assigns = [
|
|
||||||
post: :hello |> :code.priv_dir() |> Path.join("static/posts/#{id}.html") |> File.read!()
|
|
||||||
]
|
|
||||||
|
|
||||||
render(conn, :show, assigns)
|
|
||||||
end
|
|
||||||
|
|
||||||
def drafts(conn, %{"id" => id}) do
|
|
||||||
config = Application.get_env(:pandoc, :hello)
|
|
||||||
|
|
||||||
opts = [
|
|
||||||
cd: config[:cd] || File.cwd!()
|
|
||||||
]
|
|
||||||
|
|
||||||
filename = List.keyfind(@posts, id, 0) |> elem(1) |> Map.get(:filename)
|
|
||||||
path = Path.join("_drafts", filename)
|
|
||||||
|
|
||||||
render(conn, :show, post: "pandoc" |> System.cmd([path], opts) |> elem(0))
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
```elixir
|
|
||||||
# lib/hello/document.ex
|
|
||||||
|
|
||||||
defmodule Stasis.Document do
|
|
||||||
require Logger
|
|
||||||
|
|
||||||
@ext ".md"
|
|
||||||
@pattern Application.compile_env(:pandoc, [:hello, :pattern])
|
|
||||||
|
|
||||||
def list() do
|
|
||||||
"documents"
|
|
||||||
|> Path.join(@pattern)
|
|
||||||
|> Path.wildcard()
|
|
||||||
|> Enum.map(fn path -> {Path.basename(path), path} end)
|
|
||||||
|> Enum.sort(fn {basename_a, _}, {basename_b, _} -> basename_a < basename_b end)
|
|
||||||
|> Enum.reduce([], fn {filename, path}, acc ->
|
|
||||||
id = Path.rootname(filename, @ext)
|
|
||||||
data = if "_drafts" in Path.split(path), do: %{:draft, true}, else: %{}
|
|
||||||
|
|
||||||
[{id, data} | acc]
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
```elixir
|
|
||||||
# lib/hello_web/controllers/post_html.ex
|
|
||||||
|
|
||||||
# ...
|
|
||||||
|
|
||||||
defp href(filename, draft \\ false) do
|
|
||||||
root = (draft && "/drafts") || "/posts"
|
|
||||||
Path.join(root, filename |> Path.basename(".md"))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```heex
|
Now let's change `config/config.exs` to configure `pandoc` to write to
|
||||||
<!-- lib/hello_web/controllers/post_html/index.html.heex -->
|
`priv/static/posts`:
|
||||||
|
|
||||||
<%= for {id, data} <- @posts do %>
|
```elixir
|
||||||
<p>
|
config :pandoc,
|
||||||
<.link href={href(Path.rootname(filename), data[:draft])} method="get">
|
version: "3.6.1",
|
||||||
<%= id %>
|
default: [
|
||||||
</.link>
|
args: fn extra_args ->
|
||||||
</p>
|
{_, [input_file], _} = OptionParser.parse(extra_args, switches: [])
|
||||||
<% end %>
|
~w(--output=../priv/static/posts/#{Path.rootname(input_file)}.html)
|
||||||
|
end,
|
||||||
|
cd: Path.expand("../documents", __DIR__)
|
||||||
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
```heex
|
For development, we want to enable the watcher. So find the `watchers`
|
||||||
<!-- lib/hello_web/controllers/post_html/show.html.heex -->
|
configuration in your `config/dev.exs` and add:
|
||||||
|
|
||||||
<%= raw(@post) %>
|
```elixir
|
||||||
|
pandoc: {Pandoc, :watch, [:default]}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Note we are enabling the file system watcher.
|
||||||
|
|
||||||
|
## Licence
|
||||||
|
|
||||||
|
pandoc source code is licensed under the MIT License.
|
||||||
|
7
config/config.exs
Normal file
7
config/config.exs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import Config
|
||||||
|
|
||||||
|
config :pandoc,
|
||||||
|
version: "3.6.1",
|
||||||
|
another: [
|
||||||
|
args: ["--version"]
|
||||||
|
]
|
@ -1,13 +1,44 @@
|
|||||||
defmodule Mix.Tasks.Pandoc do
|
defmodule Mix.Tasks.Pandoc do
|
||||||
use Mix.Task
|
@moduledoc """
|
||||||
|
Invokes pandoc with the given args.
|
||||||
|
|
||||||
@ext ".md"
|
Usage:
|
||||||
|
|
||||||
|
$ mix pandoc TASK_OPTIONS PROFILE PANDOC_ARGS
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
$ mix pandoc default documents/hello.md -o priv/static/posts/hello.html
|
||||||
|
|
||||||
|
If pandoc is not installed, it is automatically downloaded. Note the
|
||||||
|
arguments given to this task will be appended to any configured arguments.
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
* `--runtime-config` - load the runtime configuration
|
||||||
|
before executing command
|
||||||
|
|
||||||
|
Note flags to control this Mix task must be given before the profile:
|
||||||
|
|
||||||
|
$ mix pandoc --runtime-config default documents/hello.md
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
@shortdoc "Invokes pandoc with the profile and args"
|
||||||
|
@compile {:no_warn_undefined, Mix}
|
||||||
|
|
||||||
|
use Mix.Task
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def run(args) do
|
def run(args) do
|
||||||
switches = [runtime_config: :boolean]
|
switches = [runtime_config: :boolean]
|
||||||
{opts, remaining_args} = OptionParser.parse_head!(args, switches: switches)
|
{opts, remaining_args} = OptionParser.parse_head!(args, switches: switches)
|
||||||
|
|
||||||
|
if function_exported?(Mix, :ensure_application!, 1) do
|
||||||
|
Mix.ensure_application!(:inets)
|
||||||
|
Mix.ensure_application!(:ssl)
|
||||||
|
end
|
||||||
|
|
||||||
if opts[:runtime_config] do
|
if opts[:runtime_config] do
|
||||||
Mix.Task.run("app.config")
|
Mix.Task.run("app.config")
|
||||||
else
|
else
|
||||||
@ -19,28 +50,11 @@ defmodule Mix.Tasks.Pandoc do
|
|||||||
install_and_run(remaining_args)
|
install_and_run(remaining_args)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp install_and_run([profile | _args] = all) do
|
defp install_and_run([profile | args] = all) do
|
||||||
IO.puts("Converting markdown...")
|
case Pandoc.install_and_run(String.to_atom(profile), args) do
|
||||||
|
0 -> :ok
|
||||||
profile = String.to_atom(profile)
|
status -> Mix.raise("`mix pandoc #{Enum.join(all, " ")}` exited with #{status}")
|
||||||
config = Application.get_env(:pandoc, profile)
|
end
|
||||||
args = config[:args] || []
|
|
||||||
opts = [cd: config[:cd] || File.cwd!()]
|
|
||||||
|
|
||||||
out_path = List.last(args)
|
|
||||||
full_out_path = [opts[:cd], out_path] |> Path.join() |> Path.expand()
|
|
||||||
File.rm_rf!(full_out_path)
|
|
||||||
File.mkdir_p!(full_out_path)
|
|
||||||
|
|
||||||
opts[:cd]
|
|
||||||
|> Path.join("*#{@ext}")
|
|
||||||
|> Path.wildcard()
|
|
||||||
|> Enum.each(fn path ->
|
|
||||||
case Pandoc.run(profile, path) do
|
|
||||||
0 -> :ok
|
|
||||||
status -> Mix.raise("`mix pandoc #{Enum.join(all, " ")}` exited with #{status}")
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp install_and_run([]) do
|
defp install_and_run([]) do
|
||||||
|
70
lib/mix/tasks/pandoc.install.ex
Normal file
70
lib/mix/tasks/pandoc.install.ex
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
defmodule Mix.Tasks.Pandoc.Install do
|
||||||
|
@moduledoc """
|
||||||
|
Installs pandoc under `_build`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ mix pandoc.install
|
||||||
|
$ mix pandoc.install --if-missing
|
||||||
|
```
|
||||||
|
|
||||||
|
By default, it installs #{Pandoc.latest_version()} but you can configure it
|
||||||
|
in your config files, such as:
|
||||||
|
|
||||||
|
config :pandoc, :version, "#{Pandoc.latest_version()}"
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
* `--runtime-config` - load the runtime configuration before executing
|
||||||
|
command
|
||||||
|
|
||||||
|
* `--if-missing` - install only if the given version does not exist
|
||||||
|
"""
|
||||||
|
|
||||||
|
@shortdoc "Installs pandoc under _build"
|
||||||
|
@compile {:no_warn_undefined, Mix}
|
||||||
|
|
||||||
|
use Mix.Task
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def run(args) do
|
||||||
|
valid_options = [runtime_config: :boolean, if_missing: :boolean]
|
||||||
|
|
||||||
|
{opts, base_url} =
|
||||||
|
case OptionParser.parse_head!(args, strict: valid_options) do
|
||||||
|
{opts, []} ->
|
||||||
|
{opts, Pandoc.default_base_url()}
|
||||||
|
|
||||||
|
{opts, [base_url]} ->
|
||||||
|
{opts, base_url}
|
||||||
|
|
||||||
|
{_, _} ->
|
||||||
|
Mix.raise("""
|
||||||
|
Invalid arguments to pandoc.install, expected one of:
|
||||||
|
|
||||||
|
mix pandoc.install
|
||||||
|
mix pandoc.install 'https://github.com/jgm/pandoc/releases/download/$version/pandoc-$version-$target.tar.gz'
|
||||||
|
mix pandoc.install --runtime-config
|
||||||
|
mix pandoc.install --if-missing
|
||||||
|
""")
|
||||||
|
end
|
||||||
|
|
||||||
|
if opts[:runtime_config], do: Mix.Task.run("app.config")
|
||||||
|
|
||||||
|
if opts[:if_missing] && latest_version?() do
|
||||||
|
:ok
|
||||||
|
else
|
||||||
|
if function_exported?(Mix, :ensure_application!, 1) do
|
||||||
|
Mix.ensure_application!(:inets)
|
||||||
|
Mix.ensure_application!(:ssl)
|
||||||
|
end
|
||||||
|
|
||||||
|
Mix.Task.run("loadpaths")
|
||||||
|
Pandoc.install(base_url)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp latest_version?() do
|
||||||
|
version = Pandoc.configured_version()
|
||||||
|
match?({:ok, ^version}, Pandoc.bin_version())
|
||||||
|
end
|
||||||
|
end
|
362
lib/pandoc.ex
362
lib/pandoc.ex
@ -1,17 +1,130 @@
|
|||||||
defmodule Pandoc do
|
defmodule Pandoc do
|
||||||
|
# https://github.com/jgm/pandoc/releases
|
||||||
|
@latest_version "3.6.1"
|
||||||
|
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Documentation for `Pandoc`.
|
Pandoc is an installer, runner and watcher for [pandoc](https://pandoc.org).
|
||||||
|
|
||||||
|
## Profiles
|
||||||
|
|
||||||
|
You can define multiple pandoc profiles. By default, there is a profile
|
||||||
|
called `:default` which you can configure its args, current directory and
|
||||||
|
environment. You can make the args dynamic by defining a function.
|
||||||
|
|
||||||
|
config :pandoc,
|
||||||
|
version: "#{@latest_version}",
|
||||||
|
default: [
|
||||||
|
args: fn extra_args ->
|
||||||
|
{_, [input_file], _} = OptionParser.parse(extra_args, switches: [])
|
||||||
|
~w(--output=../priv/static/posts/\#{Path.rootname(input_file)}.html)
|
||||||
|
end,
|
||||||
|
cd: Path.expand("../documents", __DIR__)
|
||||||
|
]
|
||||||
|
|
||||||
|
## Pandoc configuration
|
||||||
|
|
||||||
|
There are four global configurations for the pandoc application:
|
||||||
|
|
||||||
|
* `:version` - the expected pandoc version
|
||||||
|
|
||||||
|
* `:version_check` - whether to perform the version check or not.
|
||||||
|
Useful when you manage the pandoc executable with an external
|
||||||
|
tool
|
||||||
|
|
||||||
|
* `:cacerts_path` - the directory to find certificates for
|
||||||
|
https connections
|
||||||
|
|
||||||
|
* `:path` - the path to find the pandoc executable at. By
|
||||||
|
default, it is automatically downloaded and placed inside
|
||||||
|
the `_build` directory of your current app
|
||||||
|
|
||||||
|
Overriding the `:path` is not recommended, as we will automatically download
|
||||||
|
and manage `pandoc` for you. But in case you can't download it, you may want
|
||||||
|
to set the `:path` to a configurable system location.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def run(profile, ["--watch" | _]) do
|
require Logger
|
||||||
config = Application.get_env(:pandoc, profile)
|
|
||||||
opts = [cd: config[:cd] || File.cwd!()]
|
@doc false
|
||||||
dirs = [opts[:cd], Path.join(opts[:cd], "_drafts")]
|
# Latest known version at the time of publishing.
|
||||||
|
def latest_version, do: @latest_version
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns the configured pandoc version.
|
||||||
|
"""
|
||||||
|
def configured_version do
|
||||||
|
Application.get_env(:pandoc, :version, latest_version())
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns the configuration for the given profile.
|
||||||
|
|
||||||
|
Returns nil if the profile does not exist.
|
||||||
|
"""
|
||||||
|
def config_for!(profile) when is_atom(profile) do
|
||||||
|
Application.get_env(:pandoc, profile) ||
|
||||||
|
raise ArgumentError, """
|
||||||
|
unknown pandoc profile. Make sure the profile is defined in your config/config.exs file, such as:
|
||||||
|
|
||||||
|
config :pandoc,
|
||||||
|
version: "#{@latest_version}",
|
||||||
|
#{profile}: [
|
||||||
|
cd: Path.expand("../documents", __DIR__)
|
||||||
|
]
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns the path to the executable.
|
||||||
|
|
||||||
|
The executable may not be available if it was not yet installed.
|
||||||
|
"""
|
||||||
|
def bin_path do
|
||||||
|
name = "pandoc-#{target()}"
|
||||||
|
|
||||||
|
Application.get_env(:pandoc, :path) ||
|
||||||
|
if Code.ensure_loaded?(Mix.Project) do
|
||||||
|
relative_build_dir = Mix.Project.build_path() |> Path.dirname() |> Path.relative_to_cwd()
|
||||||
|
project_dir = Path.dirname(Mix.Project.project_file())
|
||||||
|
Path.join([project_dir, relative_build_dir, name])
|
||||||
|
else
|
||||||
|
Path.expand("_build/#{name}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns the version of the pandoc executable.
|
||||||
|
|
||||||
|
Returns `{:ok, version_string}` on success or `:error` when the executable
|
||||||
|
is not available.
|
||||||
|
"""
|
||||||
|
def bin_version do
|
||||||
|
path = bin_path()
|
||||||
|
|
||||||
|
with true <- File.exists?(path),
|
||||||
|
{out, 0} <- System.cmd(path, ["--version"]),
|
||||||
|
[vsn] <- Regex.run(~r/#{Path.basename(path)} ([^\s]+)/, out, capture: :all_but_first) do
|
||||||
|
{:ok, vsn}
|
||||||
|
else
|
||||||
|
_ -> :error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Starts a file system watcher that runs the given command with `args` when a
|
||||||
|
file event is received for a file that matches the given pattern.
|
||||||
|
|
||||||
|
The given args will be appended to the configured args. The task output will
|
||||||
|
be streamed directly to stdio.
|
||||||
|
"""
|
||||||
|
def watch(profile, extra_args \\ [], pattern \\ ~r/\.md$/) when is_atom(profile) do
|
||||||
|
config = config_for!(profile)
|
||||||
|
opts = [dirs: [config[:cd] || File.cwd!()]]
|
||||||
|
|
||||||
ref =
|
ref =
|
||||||
__MODULE__.Supervisor
|
__MODULE__.Supervisor
|
||||||
|> Supervisor.start_child(
|
|> Supervisor.start_child(
|
||||||
Supervisor.child_spec({Pandoc.Watcher, [profile, dirs: dirs]},
|
Supervisor.child_spec({Pandoc.Watcher, [profile, opts, pattern, extra_args]},
|
||||||
restart: :transient,
|
restart: :transient,
|
||||||
id: __MODULE__.Watcher
|
id: __MODULE__.Watcher
|
||||||
)
|
)
|
||||||
@ -27,9 +140,24 @@ defmodule Pandoc do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def run(profile, path) do
|
@doc """
|
||||||
config = Application.get_env(:pandoc, profile)
|
Runs the given command with `args`.
|
||||||
args = config[:args] || []
|
|
||||||
|
The given args will be appended to the configured args. The task output will
|
||||||
|
be streamed directly to stdio. It returns the status of the underlying call.
|
||||||
|
"""
|
||||||
|
def run(profile, extra_args) when is_atom(profile) and is_list(extra_args) do
|
||||||
|
config = config_for!(profile)
|
||||||
|
|
||||||
|
args =
|
||||||
|
case config[:args] do
|
||||||
|
args_fn when is_function(args_fn) -> args_fn.(extra_args)
|
||||||
|
args -> args || []
|
||||||
|
end
|
||||||
|
|
||||||
|
if args == [] and extra_args == [] do
|
||||||
|
raise "no arguments passed to pandoc"
|
||||||
|
end
|
||||||
|
|
||||||
opts = [
|
opts = [
|
||||||
cd: config[:cd] || File.cwd!(),
|
cd: config[:cd] || File.cwd!(),
|
||||||
@ -37,17 +165,215 @@ defmodule Pandoc do
|
|||||||
stderr_to_stdout: true
|
stderr_to_stdout: true
|
||||||
]
|
]
|
||||||
|
|
||||||
new_filename =
|
{parsed_args, _, _} = OptionParser.parse(args, switches: [output: :string])
|
||||||
path |> Path.basename() |> String.replace_suffix(".md", ".html") |> String.slice(11..-1//1)
|
{_, input_files, _} = OptionParser.parse(extra_args, switches: [])
|
||||||
|
|
||||||
new_path = args |> List.last() |> Path.join(new_filename)
|
if parsed_args[:output] &&
|
||||||
out_path = Path.join(opts[:cd], new_path) |> Path.expand()
|
not File.cd!(opts[:cd], fn ->
|
||||||
|
input_files |> Enum.map(&File.exists?(&1)) |> Enum.all?()
|
||||||
if File.exists?(path) do
|
end) do
|
||||||
args = List.replace_at(args, -1, out_path)
|
parsed_args[:output] |> Path.expand(opts[:cd]) |> File.rm!()
|
||||||
"pandoc" |> System.cmd(args ++ [path], opts) |> elem(1)
|
|
||||||
else
|
else
|
||||||
File.rm(out_path)
|
bin_path() |> System.cmd(args ++ extra_args, opts) |> elem(1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp start_unique_install_worker() do
|
||||||
|
ref =
|
||||||
|
__MODULE__.Supervisor
|
||||||
|
|> Supervisor.start_child(
|
||||||
|
Supervisor.child_spec({Task, &install/0}, restart: :transient, id: __MODULE__.Installer)
|
||||||
|
)
|
||||||
|
|> case do
|
||||||
|
{:ok, pid} -> pid
|
||||||
|
{:error, {:already_started, pid}} -> pid
|
||||||
|
end
|
||||||
|
|> Process.monitor()
|
||||||
|
|
||||||
|
receive do
|
||||||
|
{:DOWN, ^ref, _, _, _} -> :ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Installs, if not available, and then runs `pandoc`.
|
||||||
|
|
||||||
|
This task may be invoked concurrently and it will avoid concurrent installs.
|
||||||
|
|
||||||
|
Returns the same as `run/2`.
|
||||||
|
"""
|
||||||
|
def install_and_run(profile, args) do
|
||||||
|
File.exists?(bin_path()) || start_unique_install_worker()
|
||||||
|
|
||||||
|
run(profile, args)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
The default URL to install Pandoc from.
|
||||||
|
"""
|
||||||
|
def default_base_url do
|
||||||
|
"https://github.com/jgm/pandoc/releases/download/$version/pandoc-$version-$target.tar.gz"
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Installs pandoc with `configured_version/0`.
|
||||||
|
|
||||||
|
If invoked concurrently, this task will perform concurrent installs.
|
||||||
|
"""
|
||||||
|
def install(base_url \\ default_base_url()) do
|
||||||
|
version = configured_version()
|
||||||
|
tmp_opts = if System.get_env("MIX_XDG"), do: %{os: :linux}, else: %{}
|
||||||
|
|
||||||
|
tmp_dir =
|
||||||
|
freshdir_p(:filename.basedir(:user_cache, "phx-pandoc", tmp_opts)) ||
|
||||||
|
freshdir_p(Path.join(System.tmp_dir!(), "phx-pandoc")) ||
|
||||||
|
raise "could not install pandoc. Set MIX_XGD=1 and then set XDG_CACHE_HOME to the path you want to use as cache"
|
||||||
|
|
||||||
|
url = get_url(base_url)
|
||||||
|
tar = fetch_body!(url)
|
||||||
|
|
||||||
|
case :erl_tar.extract({:binary, tar}, [:compressed, cwd: to_charlist(tmp_dir)]) do
|
||||||
|
:ok -> :ok
|
||||||
|
other -> raise "couldn't unpack archive: #{inspect(other)}"
|
||||||
|
end
|
||||||
|
|
||||||
|
bin_path = bin_path()
|
||||||
|
File.mkdir_p!(Path.dirname(bin_path))
|
||||||
|
[tmp_dir, "pandoc-" <> version, "bin", "pandoc"] |> Path.join() |> File.cp!(bin_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp freshdir_p(path) do
|
||||||
|
with {:ok, _} <- File.rm_rf(path),
|
||||||
|
:ok <- File.mkdir_p(path) do
|
||||||
|
path
|
||||||
|
else
|
||||||
|
_ -> nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp fetch_body!(url, retry \\ true) do
|
||||||
|
scheme = URI.parse(url).scheme
|
||||||
|
url = String.to_charlist(url)
|
||||||
|
Logger.debug("Downloading pandoc from #{url}")
|
||||||
|
|
||||||
|
{:ok, _} = Application.ensure_all_started(:inets)
|
||||||
|
{:ok, _} = Application.ensure_all_started(:ssl)
|
||||||
|
|
||||||
|
if proxy = proxy_for_scheme(scheme) do
|
||||||
|
%{host: host, port: port} = URI.parse(proxy)
|
||||||
|
Logger.debug("Using #{String.upcase(scheme)}_PROXY: #{proxy}")
|
||||||
|
set_option = if "https" == scheme, do: :https_proxy, else: :proxy
|
||||||
|
:httpc.set_options([{set_option, {{String.to_charlist(host), port}, []}}])
|
||||||
|
end
|
||||||
|
|
||||||
|
# https://erlef.github.io/security-wg/secure_coding_and_deployment_hardening/inets
|
||||||
|
cacertfile = cacertfile() |> String.to_charlist()
|
||||||
|
|
||||||
|
http_options =
|
||||||
|
[
|
||||||
|
ssl: [
|
||||||
|
verify: :verify_peer,
|
||||||
|
cacertfile: cacertfile,
|
||||||
|
depth: 2,
|
||||||
|
customize_hostname_check: [
|
||||||
|
match_fun: :public_key.pkix_verify_hostname_match_fun(:https)
|
||||||
|
],
|
||||||
|
versions: protocol_versions()
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|> maybe_add_proxy_auth(scheme)
|
||||||
|
|
||||||
|
options = [body_format: :binary]
|
||||||
|
|
||||||
|
case {retry, :httpc.request(:get, {url, []}, http_options, options)} do
|
||||||
|
{_, {:ok, {{_, 200, _}, _headers, body}}} ->
|
||||||
|
body
|
||||||
|
|
||||||
|
{true, {:error, {:failed_connect, [{:to_address, _}, {inet, _, reason}]}}}
|
||||||
|
when inet in [:inet, :inet6] and
|
||||||
|
reason in [:ehostunreach, :enetunreach, :eprotonosupport, :nxdomain] ->
|
||||||
|
:httpc.set_options(ipfamily: fallback(inet))
|
||||||
|
fetch_body!(url, false)
|
||||||
|
|
||||||
|
other ->
|
||||||
|
raise """
|
||||||
|
Couldn't fetch #{url}: #{inspect(other)}
|
||||||
|
|
||||||
|
This typically means we cannot reach the source or you are behind a proxy.
|
||||||
|
You can try again later and, if that does not work, you might:
|
||||||
|
|
||||||
|
1. If behind a proxy, ensure your proxy is configured and that
|
||||||
|
your certificates are set via the cacerts_path configuration
|
||||||
|
|
||||||
|
2. Manually download the executable from the URL above and
|
||||||
|
place it inside "_build/pandoc-#{target()}"
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp fallback(:inet), do: :inet6
|
||||||
|
defp fallback(:inet6), do: :inet
|
||||||
|
|
||||||
|
defp proxy_for_scheme("http") do
|
||||||
|
System.get_env("HTTP_PROXY") || System.get_env("http_proxy")
|
||||||
|
end
|
||||||
|
|
||||||
|
defp proxy_for_scheme("https") do
|
||||||
|
System.get_env("HTTPS_PROXY") || System.get_env("https_proxy")
|
||||||
|
end
|
||||||
|
|
||||||
|
defp maybe_add_proxy_auth(http_options, scheme) do
|
||||||
|
case proxy_auth(scheme) do
|
||||||
|
nil -> http_options
|
||||||
|
auth -> [{:proxy_auth, auth} | http_options]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp proxy_auth(scheme) do
|
||||||
|
with proxy when is_binary(proxy) <- proxy_for_scheme(scheme),
|
||||||
|
%{userinfo: userinfo} when is_binary(userinfo) <- URI.parse(proxy),
|
||||||
|
[username, password] <- String.split(userinfo, ":") do
|
||||||
|
{String.to_charlist(username), String.to_charlist(password)}
|
||||||
|
else
|
||||||
|
_ -> nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp cacertfile() do
|
||||||
|
Application.get_env(:pandoc, :cacerts_path) || CAStore.file_path()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp protocol_versions do
|
||||||
|
if otp_version() < 25, do: [:"tlsv1.2"], else: [:"tlsv1.2", :"tlsv1.3"]
|
||||||
|
end
|
||||||
|
|
||||||
|
defp otp_version do
|
||||||
|
:erlang.system_info(:otp_release) |> List.to_integer()
|
||||||
|
end
|
||||||
|
|
||||||
|
# Available targets: https://github.com/jgm/pandoc/releases
|
||||||
|
# We support only linux-amd64, for now.
|
||||||
|
defp target do
|
||||||
|
case :os.type() do
|
||||||
|
# Assuming it's an x86 CPU
|
||||||
|
{:win32, _} ->
|
||||||
|
raise "pandoc does not currently support OS family: Windows"
|
||||||
|
|
||||||
|
{:unix, osname} ->
|
||||||
|
arch_str = :erlang.system_info(:system_architecture)
|
||||||
|
[arch | _] = arch_str |> List.to_string() |> String.split("-")
|
||||||
|
|
||||||
|
case arch do
|
||||||
|
"amd64" -> "#{osname}-amd64"
|
||||||
|
"x86_64" -> "#{osname}-amd64"
|
||||||
|
_ -> raise "pandoc does not currently support architecture: #{arch_str}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_url(base_url) do
|
||||||
|
base_url
|
||||||
|
|> String.replace("$version", configured_version())
|
||||||
|
|> String.replace("$target", target())
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -5,16 +5,37 @@ defmodule Pandoc.Application do
|
|||||||
|
|
||||||
use Application
|
use Application
|
||||||
|
|
||||||
@impl true
|
require Logger
|
||||||
def start(_type, _args) do
|
import Pandoc, only: [latest_version: 0, configured_version: 0, bin_version: 0]
|
||||||
children = [
|
|
||||||
# Starts a worker by calling: Pandoc.Worker.start_link(arg)
|
|
||||||
# {Pandoc.Worker, arg}
|
|
||||||
]
|
|
||||||
|
|
||||||
# See https://hexdocs.pm/elixir/Supervisor.html
|
@doc false
|
||||||
# for other strategies and supported options
|
def start(_, _) do
|
||||||
opts = [strategy: :one_for_one, name: Pandoc.Supervisor]
|
if Application.get_env(:pandoc, :version_check, true) do
|
||||||
Supervisor.start_link(children, opts)
|
unless Application.get_env(:pandoc, :version) do
|
||||||
|
Logger.warning("""
|
||||||
|
pandoc version is not configured. Please set it in your config files:
|
||||||
|
|
||||||
|
config :pandoc, :version, "#{latest_version()}"
|
||||||
|
""")
|
||||||
|
end
|
||||||
|
|
||||||
|
configured_version = configured_version()
|
||||||
|
|
||||||
|
case bin_version() do
|
||||||
|
{:ok, ^configured_version} ->
|
||||||
|
:ok
|
||||||
|
|
||||||
|
{:ok, version} ->
|
||||||
|
Logger.warning("""
|
||||||
|
Outdated pandoc version. Expected #{configured_version}, got #{version}. \
|
||||||
|
Please run `mix pandoc.install` or update the version in your config files.\
|
||||||
|
""")
|
||||||
|
|
||||||
|
:error ->
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Supervisor.start_link([], strategy: :one_for_one, name: Pandoc.Supervisor)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,22 +1,29 @@
|
|||||||
defmodule Pandoc.Watcher do
|
defmodule Pandoc.Watcher do
|
||||||
use GenServer
|
@moduledoc false
|
||||||
|
|
||||||
@ext ".md"
|
use GenServer
|
||||||
|
|
||||||
def start_link(args) do
|
def start_link(args) do
|
||||||
GenServer.start_link(__MODULE__, args)
|
GenServer.start_link(__MODULE__, args)
|
||||||
end
|
end
|
||||||
|
|
||||||
def init([profile | args]) do
|
# Callbacks
|
||||||
{:ok, watcher_pid} = FileSystem.start_link(args)
|
|
||||||
|
@impl true
|
||||||
|
def init([profile, options, pattern, extra_args]) do
|
||||||
|
{:ok, watcher_pid} = FileSystem.start_link(options)
|
||||||
FileSystem.subscribe(watcher_pid)
|
FileSystem.subscribe(watcher_pid)
|
||||||
{:ok, %{watcher_pid: watcher_pid, profile: profile}}
|
{:ok, %{watcher_pid: watcher_pid, profile: profile, pattern: pattern, extra_args: extra_args}}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
def handle_info({:file_event, watcher_pid, {path, events}}, %{watcher_pid: watcher_pid} = state) do
|
def handle_info({:file_event, watcher_pid, {path, events}}, %{watcher_pid: watcher_pid} = state) do
|
||||||
case {Path.extname(path), :closed in events or :deleted in events} do
|
case {String.match?(path, state[:pattern]), :closed in events or :deleted in events} do
|
||||||
{@ext, true} -> Pandoc.run(state[:profile], path)
|
{true, true} ->
|
||||||
_ -> nil
|
Pandoc.install_and_run(state[:profile], [Path.basename(path) | state[:extra_args]])
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
{:noreply, state}
|
{:noreply, state}
|
||||||
|
17
mix.exs
17
mix.exs
@ -1,7 +1,7 @@
|
|||||||
defmodule Pandoc.MixProject do
|
defmodule Pandoc.MixProject do
|
||||||
use Mix.Project
|
use Mix.Project
|
||||||
|
|
||||||
@version "0.2.0"
|
@version "0.3.0"
|
||||||
@source_url "https://webdevcat.me/git/pandoc/"
|
@source_url "https://webdevcat.me/git/pandoc/"
|
||||||
|
|
||||||
def project do
|
def project do
|
||||||
@ -10,22 +10,28 @@ defmodule Pandoc.MixProject do
|
|||||||
version: @version,
|
version: @version,
|
||||||
elixir: "~> 1.14",
|
elixir: "~> 1.14",
|
||||||
deps: deps(),
|
deps: deps(),
|
||||||
description: "File-watcher and Mix task to convert Markdown files to HTML",
|
description: "File system watcher and Mix tasks for installing and invoking pandoc",
|
||||||
package: [
|
package: [
|
||||||
links: %{
|
links: %{
|
||||||
"pandoc" => "https://pandoc.org/"
|
"pandoc" => "https://pandoc.org/"
|
||||||
},
|
},
|
||||||
licenses: ["MIT"]
|
licenses: ["MIT"]
|
||||||
],
|
],
|
||||||
source_url: @source_url
|
source_url: @source_url,
|
||||||
|
source_url_pattern: "#{@source_url}tree/%{path}#n%{line}",
|
||||||
|
docs: [
|
||||||
|
main: "Pandoc"
|
||||||
|
],
|
||||||
|
aliases: [test: ["pandoc.install --if-missing", "test"]]
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
# Run "mix help compile.app" to learn about applications.
|
# Run "mix help compile.app" to learn about applications.
|
||||||
def application do
|
def application do
|
||||||
[
|
[
|
||||||
extra_applications: [:logger],
|
extra_applications: [:logger, inets: :optional, ssl: :optional],
|
||||||
mod: {Pandoc.Application, []}
|
mod: {Pandoc.Application, []},
|
||||||
|
env: [default: []]
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -33,6 +39,7 @@ defmodule Pandoc.MixProject do
|
|||||||
defp deps do
|
defp deps do
|
||||||
[
|
[
|
||||||
{:file_system, "~> 1.0"},
|
{:file_system, "~> 1.0"},
|
||||||
|
{:castore, ">= 0.0.0"},
|
||||||
{:ex_doc, ">= 0.0.0", only: :dev, runtime: false}
|
{:ex_doc, ">= 0.0.0", only: :dev, runtime: false}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
1
mix.lock
1
mix.lock
@ -1,4 +1,5 @@
|
|||||||
%{
|
%{
|
||||||
|
"castore": {:hex, :castore, "1.0.10", "43bbeeac820f16c89f79721af1b3e092399b3a1ecc8df1a472738fd853574911", [:mix], [], "hexpm", "1b0b7ea14d889d9ea21202c43a4fa015eb913021cb535e8ed91946f4b77a8848"},
|
||||||
"earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"},
|
"earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"},
|
||||||
"ex_doc": {:hex, :ex_doc, "0.35.1", "de804c590d3df2d9d5b8aec77d758b00c814b356119b3d4455e4b8a8687aecaf", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "2121c6402c8d44b05622677b761371a759143b958c6c19f6558ff64d0aed40df"},
|
"ex_doc": {:hex, :ex_doc, "0.35.1", "de804c590d3df2d9d5b8aec77d758b00c814b356119b3d4455e4b8a8687aecaf", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "2121c6402c8d44b05622677b761371a759143b958c6c19f6558ff64d0aed40df"},
|
||||||
"file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"},
|
"file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"},
|
||||||
|
@ -1,8 +1,111 @@
|
|||||||
defmodule PandocTest do
|
defmodule PandocTest do
|
||||||
use ExUnit.Case
|
use ExUnit.Case, async: true
|
||||||
doctest Pandoc
|
|
||||||
|
|
||||||
test "greets the world" do
|
alias Pandoc.Watcher
|
||||||
assert Pandoc.hello() == :world
|
import ExUnit.CaptureIO
|
||||||
|
|
||||||
|
@version Pandoc.latest_version()
|
||||||
|
|
||||||
|
test "run on default" do
|
||||||
|
assert capture_io(fn ->
|
||||||
|
assert Pandoc.run(:default, ["--version"]) == 0
|
||||||
|
end) =~ @version
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "run on profile" do
|
||||||
|
assert capture_io(fn ->
|
||||||
|
assert Pandoc.run(:another, []) == 0
|
||||||
|
end) =~ @version
|
||||||
|
end
|
||||||
|
|
||||||
|
test "updates on install" do
|
||||||
|
Application.put_env(:pandoc, :version, "3.6")
|
||||||
|
|
||||||
|
Mix.Task.rerun("pandoc.install", ["--if-missing"])
|
||||||
|
|
||||||
|
assert capture_io(fn ->
|
||||||
|
assert Pandoc.run(:default, ["--version"]) == 0
|
||||||
|
end) =~ "3.6"
|
||||||
|
|
||||||
|
Application.delete_env(:pandoc, :version)
|
||||||
|
|
||||||
|
Mix.Task.rerun("pandoc.install", ["--if-missing"])
|
||||||
|
|
||||||
|
assert capture_io(fn ->
|
||||||
|
assert Pandoc.run(:default, ["--version"]) == 0
|
||||||
|
end) =~ @version
|
||||||
|
after
|
||||||
|
Application.delete_env(:pandoc, :version)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "install and run multiple concurrently" do
|
||||||
|
bin_path = Pandoc.bin_path()
|
||||||
|
|
||||||
|
assert :ok = File.exists?(bin_path) && File.rm!(bin_path)
|
||||||
|
|
||||||
|
results =
|
||||||
|
[:extra1, :extra2, :extra3]
|
||||||
|
|> Enum.map(fn profile ->
|
||||||
|
Application.put_env(:pandoc, profile, args: ["--version"])
|
||||||
|
|
||||||
|
Task.async(fn ->
|
||||||
|
capture_io(fn ->
|
||||||
|
ret_code = Pandoc.install_and_run(profile, [])
|
||||||
|
# Let the first finished task set the binary file to read and execute only,
|
||||||
|
# so that the others will fail if they try to overwrite it.
|
||||||
|
File.chmod!(bin_path, 0o500)
|
||||||
|
ret_code == 0
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|> Task.await_many(:infinity)
|
||||||
|
|
||||||
|
File.chmod!(bin_path, 0o700)
|
||||||
|
assert Enum.all?(results)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "installs with custom URL" do
|
||||||
|
assert :ok =
|
||||||
|
Mix.Task.rerun("pandoc.install", [
|
||||||
|
"https://github.com/jgm/pandoc/releases/download/$version/pandoc-$version-$target.tar.gz"
|
||||||
|
])
|
||||||
|
end
|
||||||
|
|
||||||
|
test "starts watching and writes to stdio" do
|
||||||
|
in_tmp("documents", fn ->
|
||||||
|
task = Task.async(fn -> Pandoc.watch(:default) end)
|
||||||
|
%{pid: pid, ref: ref} = task
|
||||||
|
:timer.sleep(200)
|
||||||
|
|
||||||
|
{_, watcher_pid, _, _} =
|
||||||
|
Pandoc.Supervisor
|
||||||
|
|> Supervisor.which_children()
|
||||||
|
|> Enum.find(fn
|
||||||
|
{Watcher, _, _, _} -> true
|
||||||
|
_ -> false
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert capture_io(fn ->
|
||||||
|
# Redirect watcher output to current process so it can be captured
|
||||||
|
Process.group_leader(watcher_pid, Process.group_leader())
|
||||||
|
File.write!("hello.md", "# hello")
|
||||||
|
Task.yield(task, 200)
|
||||||
|
end) =~ ~s{<h1 id="hello">hello</h1>\n}
|
||||||
|
|
||||||
|
assert :ok = Supervisor.terminate_child(Pandoc.Supervisor, Watcher)
|
||||||
|
assert_receive {:DOWN, ^ref, :process, ^pid, :normal}, 1000
|
||||||
|
end)
|
||||||
|
after
|
||||||
|
File.rm_rf!(tmp_path())
|
||||||
|
end
|
||||||
|
|
||||||
|
defp in_tmp(which, function) do
|
||||||
|
path = tmp_path(which)
|
||||||
|
File.rm_rf!(path)
|
||||||
|
File.mkdir_p!(path)
|
||||||
|
File.cd!(path, function)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp tmp_path, do: Path.expand("../tmp", __DIR__)
|
||||||
|
defp tmp_path(extension), do: Path.join(tmp_path(), extension)
|
||||||
end
|
end
|
||||||
|
Loading…
x
Reference in New Issue
Block a user