diff --git a/README.md b/README.md index b8dcf18..6094325 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![Latest Version](https://img.shields.io/hexpm/v/simple_blog?color=b5a3be&label=Latest+version)](https://hexdocs.pm/simple_blog) + # Simple Blog - A blog engine written in elixir to generate static blogs from markdown. @@ -39,7 +39,7 @@ $ mix deps.get ### Generate new blog post ```console -$ mix simple_blog.post.new "10 tips for new developers" +$ mix simple_blog.post "10 tips for new developers" ``` The file will be created at `blog/_posts/yyyy-mm-dd-10-tips-for-new-developers.md`. @@ -50,7 +50,7 @@ The local http server is designed to local development of your blog. To start it ```console $ mix clean -$ mix simple_blog.server.start +$ mix simple_blog.server ``` The server will be running at `http://localhost:4000`. diff --git a/lib/flat_files.ex b/lib/flat_files.ex deleted file mode 100644 index e157eac..0000000 --- a/lib/flat_files.ex +++ /dev/null @@ -1,21 +0,0 @@ -defmodule FlatFiles do - def list_all(filepath) do - _list_all(filepath) - end - - defp _list_all(filepath) do - cond do - String.contains?(filepath, ".git") -> [] - true -> expand(File.ls(filepath), filepath) - end - end - - defp expand({:ok, files}, path) do - files - |> Enum.flat_map(&_list_all("#{path}/#{&1}")) - end - - defp expand({:error, _}, path) do - [path] - end -end diff --git a/lib/mix/tasks/simple_blog/compile.ex b/lib/mix/tasks/simple_blog/compile.ex index cefd931..cb3138d 100644 --- a/lib/mix/tasks/simple_blog/compile.ex +++ b/lib/mix/tasks/simple_blog/compile.ex @@ -3,25 +3,34 @@ defmodule Mix.Tasks.SimpleBlog.Compile do require Logger @moduledoc """ - Module responsible for transpile markdown into html + Command responsible for transpile markdown into html """ + @doc """ + Generates a static blog at output folder + + ## Examples + + iex> Mix.Tasks.SimpleBlog.Compile.run([]) + """ @impl Mix.Task - def run([]) do + def run([]), do: run(["blog", "output"]) + + def run([root_directory, output_directory]) do posts = - "blog" + root_directory |> SimpleBlog.Reader.Posts.read_from_dir() |> SimpleBlog.Converter.Posts.markdown_to_html() |> Enum.map(&SimpleBlog.Post.parse(&1)) index_html = - File.read("blog/index.html.eex") + File.read(root_directory <> "/index.html.eex") |> SimpleBlog.Converter.Page.exx_to_html(posts) |> rewrite_stylesheets() |> rewrite_images() - File.mkdir("output") - {:ok, file} = File.open("output/index.html", [:write]) + File.mkdir(output_directory) + {:ok, file} = File.open(output_directory <> "/index.html", [:write]) index_html |> String.split("\n") @@ -29,10 +38,10 @@ defmodule Mix.Tasks.SimpleBlog.Compile do File.close(file) - File.cp_r("blog/css", "output/css") - File.cp_r("blog/images", "output/images") + File.cp_r(root_directory <> "/css", output_directory <> "/css") + File.cp_r(root_directory <> "/images", output_directory <> "/images") - write_html_posts(posts) + write_html_posts(root_directory, output_directory, posts) end defp rewrite_stylesheets(html) do @@ -104,34 +113,33 @@ defmodule Mix.Tasks.SimpleBlog.Compile do end end - defp write_html_posts(posts) do + defp write_html_posts(root_directory, output_directory, posts) do posts - |> Enum.map(&create_folders/1) - |> IO.inspect() + |> Enum.map(&create_folders(&1, output_directory)) posts - |> Enum.map(&create_posts_html/1) + |> Enum.map(&create_posts_html(&1, root_directory, output_directory)) end - defp create_folders(post) do + defp create_folders(post, output_directory) do post - |> SimpleBlog.Post.generate_html_dir("output/posts/") + |> SimpleBlog.Post.generate_html_dir(output_directory <> "/posts/") |> File.mkdir_p() end - def create_posts_html(post) do - dir = SimpleBlog.Post.generate_html_dir(post, "output/posts/") + def create_posts_html(post, root_directory, output_directory) do + dir = SimpleBlog.Post.generate_html_dir(post, output_directory <> "/posts/") filename = SimpleBlog.Post.generate_html_filename(post) postname = SimpleBlog.Post.generate_filename(post) post = - "blog" + root_directory |> SimpleBlog.Reader.Posts.read_post(postname) |> SimpleBlog.Converter.Posts.markdown_to_html() |> SimpleBlog.Post.parse() result = - File.read("blog/post.html.eex") + File.read(root_directory <> "/post.html.eex") |> SimpleBlog.Converter.Page.exx_to_html(post) |> rewrite_stylesheets_post() |> rewrite_images_post() diff --git a/lib/mix/tasks/simple_blog/post/new.ex b/lib/mix/tasks/simple_blog/post.ex similarity index 72% rename from lib/mix/tasks/simple_blog/post/new.ex rename to lib/mix/tasks/simple_blog/post.ex index 582b7f0..9ce36a5 100644 --- a/lib/mix/tasks/simple_blog/post/new.ex +++ b/lib/mix/tasks/simple_blog/post.ex @@ -1,17 +1,17 @@ -defmodule Mix.Tasks.SimpleBlog.Post.New do +defmodule Mix.Tasks.SimpleBlog.Post do use Mix.Task @moduledoc """ - Module responsible for generate a new blog post. + Command responsible for generate a new blog post. """ @doc """ - It generates a new blog post + Generates a new blog post ## Examples - # iex> Mix.Tasks.SimpleBlog.Post.New.run(["My first blog post"]) - # :ok + iex> Mix.Tasks.SimpleBlog.Post.run(["My first blog post"]) + "Blog post created at blog_test/_posts/yyyy-mm-dd-my-first-blog-post.md" """ @impl Mix.Task def run([]), do: Mix.shell().info(usage()) @@ -35,6 +35,10 @@ defmodule Mix.Tasks.SimpleBlog.Post.New do IO.binwrite(file, "--->" <> "\n") File.close(file) + Mix.shell().info(""" + Blog post created at #{full_file_path} + """) + {:error, :enoent} -> Mix.shell().info(""" The directory #{root_directory} was not found @@ -42,6 +46,9 @@ defmodule Mix.Tasks.SimpleBlog.Post.New do end end + @doc """ + Returns instructions about command usage + """ def usage() do """ To generate a new blog post you should pass a title as string: diff --git a/lib/mix/tasks/simple_blog/server/start.ex b/lib/mix/tasks/simple_blog/server.ex similarity index 57% rename from lib/mix/tasks/simple_blog/server/start.ex rename to lib/mix/tasks/simple_blog/server.ex index f7d6d23..0518f10 100644 --- a/lib/mix/tasks/simple_blog/server/start.ex +++ b/lib/mix/tasks/simple_blog/server.ex @@ -1,11 +1,19 @@ -defmodule Mix.Tasks.SimpleBlog.Server.Start do +defmodule Mix.Tasks.SimpleBlog.Server do use Mix.Task require Logger @moduledoc """ - Module responsible for a simple http server to allow local working on blog + Command responsible for a simple http server to allow local working on blog """ + @doc """ + Starts a local HTTP server in the port 4000 + + ## Examples + + iex> Mix.Tasks.SimpleBlog.Server.run([]) + "Server running on localhost:4000" + """ @impl Mix.Task def run([]) do webserver = [ diff --git a/lib/simple_blog.ex b/lib/simple_blog.ex deleted file mode 100644 index 0537502..0000000 --- a/lib/simple_blog.ex +++ /dev/null @@ -1,18 +0,0 @@ -defmodule SimpleBlog do - @moduledoc """ - Documentation for `SimpleBlog`. - """ - - @doc """ - Hello world. - - ## Examples - - iex> SimpleBlog.hello() - :world - - """ - def hello do - :world - end -end diff --git a/lib/simple_blog/converter/page.ex b/lib/simple_blog/converter/page.ex index dd8a10d..ce2a11f 100644 --- a/lib/simple_blog/converter/page.ex +++ b/lib/simple_blog/converter/page.ex @@ -1,4 +1,21 @@ defmodule SimpleBlog.Converter.Page do + @moduledoc """ + Module responsible for convert pages from eex to html + """ + + @doc """ + Convert eex file to html + + ## Examples + + iex> posts = [%SimpleBlog.Post{title: "post 1"}, %SimpleBlog.Post{title: "post 2"}] + iex> SimpleBlog.Converter.Page.exx_to_html({:ok, "<%= for post <- posts do %><%= post.title %><% end %>"}, posts) + "post 1post 2" + + iex> post = %SimpleBlog.Post{title: "post 1"} + iex> SimpleBlog.Converter.Page.exx_to_html({:ok, "<%= post.title %>"}, post) + "post 1" + """ def exx_to_html({:ok, body}, posts) when is_list(posts) do quoted = EEx.compile_string(body) diff --git a/lib/simple_blog/converter/posts.ex b/lib/simple_blog/converter/posts.ex index 70c893f..645e8f6 100644 --- a/lib/simple_blog/converter/posts.ex +++ b/lib/simple_blog/converter/posts.ex @@ -1,6 +1,24 @@ defmodule SimpleBlog.Converter.Posts do require Earmark + @moduledoc """ + Module responsible for convert posts from markdown to html + """ + + @doc """ + Convert markdown file to html + + ## Examples + + iex> SimpleBlog.Converter.Posts.markdown_to_html(["# post1", "# post2"]) + ["

\\npost1

\\n", "

\\npost2

\\n"] + + iex> SimpleBlog.Converter.Posts.markdown_to_html("# post1") + "

\\npost1

\\n" + + iex> SimpleBlog.Converter.Posts.markdown_to_html([]) + [] + """ def markdown_to_html([]), do: [] def markdown_to_html(files) when is_list(files) do diff --git a/lib/simple_blog/post.ex b/lib/simple_blog/post.ex index bab981e..040cb53 100644 --- a/lib/simple_blog/post.ex +++ b/lib/simple_blog/post.ex @@ -5,15 +5,15 @@ defmodule SimpleBlog.Post do @extension "md" - defstruct title: "", tags: [], body: "", date: "", filename: "" + defstruct title: "", body: "", date: "", filename: "" @doc """ - Generate filename for blog post + Generate directory name for blog post ## Examples - iex> SimpleBlog.Post.generate_filename(%SimpleBlog.Post{ title: "My first blog post", date: ~D[2023-10-04] }) - "2023-10-04-my-first-blog-post.md" + iex> SimpleBlog.Post.generate_filename(%SimpleBlog.Post{ title: "My first blog post", date: ~D[2023-10-04]}) + "2023-10-04-my-first-blog-post.md" """ def generate_filename(%SimpleBlog.Post{title: title, date: date}) do normalized_title = @@ -24,6 +24,18 @@ defmodule SimpleBlog.Post do "#{date}-#{normalized_title}.#{@extension}" end + @doc """ + Parse comment with data into %SimpleBlog.Post struct + + ## Examples + iex> body = "" + iex> SimpleBlog.Post.parse(body) + %SimpleBlog.Post{body: body, title: "Dev onboarding", date: "2023-10-25", filename: "2023-10-25-dev-onboarding.md"} + """ def parse(body) do [_, filename_line, title_line, date_line | _] = String.split(body, "\n") @@ -34,14 +46,31 @@ defmodule SimpleBlog.Post do %SimpleBlog.Post{body: body, title: title, date: date, filename: filename} end + @doc """ + Generate filename for blog post + + ## Examples + + iex> SimpleBlog.Post.generate_html_dir(%SimpleBlog.Post{date: "2023-10-04"}, "output") + "output/2023/10/04/" + """ def generate_html_dir(%SimpleBlog.Post{date: date}, base_dir) do [year, month, day] = String.split(date, "-") base_dir <> "/" <> year <> "/" <> month <> "/" <> day <> "/" end + @doc """ + Generate html filename for blog post + + ## Examples + + iex> SimpleBlog.Post.generate_html_filename(%SimpleBlog.Post{title: "doctests with elixir"}) + "doctests-with-elixir.html" + """ def generate_html_filename(%SimpleBlog.Post{title: title}) do title |> String.replace(" ", "-") + |> String.downcase() |> Kernel.<>(".html") end end diff --git a/lib/simple_blog/reader/posts.ex b/lib/simple_blog/reader/posts.ex index fed5457..0181dca 100644 --- a/lib/simple_blog/reader/posts.ex +++ b/lib/simple_blog/reader/posts.ex @@ -1,6 +1,16 @@ defmodule SimpleBlog.Reader.Posts do - require Logger + @moduledoc """ + Module responsible for read blog posts + """ + @doc """ + Reads content from markdown files located in _posts + + ## Examples + + iex> SimpleBlog.Reader.Posts.read_from_dir("blog") + ["## post title 1", "## post title 2"] + """ def read_from_dir(root_directory) do posts_directory = root_directory <> "/_posts/" @@ -10,6 +20,14 @@ defmodule SimpleBlog.Reader.Posts do end end + @doc """ + Reads content from markdown file for specific post + + ## Examples + + iex> SimpleBlog.Reader.Posts.read_post("blog", "2023-10-25-metaprogramming-in-ruby.md") + "### Metaprogramming in ruby" + """ def read_post(root_directory, post) do posts_directory = root_directory <> "/_posts/" post_path = posts_directory <> post diff --git a/lib/simple_blog/server.ex b/lib/simple_blog/server.ex index 8d0ace5..4ff43ee 100644 --- a/lib/simple_blog/server.ex +++ b/lib/simple_blog/server.ex @@ -7,10 +7,20 @@ defmodule SimpleBlog.Server do require Logger require Earmark + @moduledoc """ + Module responsible for handle HTTP requests + """ + + @doc """ + Initializes server passing initial params + """ def init(_options) do Logger.info("Initializing server ...") end + @doc """ + Handle HTTP requests. + """ def call(%Plug.Conn{request_path: "/posts/", query_string: query_string} = conn, _opts) do postname = query_string diff --git a/mix.exs b/mix.exs index 4fd77da..a4ea9bb 100644 --- a/mix.exs +++ b/mix.exs @@ -34,7 +34,7 @@ defmodule SimpleBlog.MixProject do [ {:plug_cowboy, "~> 2.0"}, {:earmark, "~> 1.4"}, - {:ex_doc, "~> 0.27", only: :dev, runtime: false}, + {:ex_doc, "~> 0.27", only: :dev, runtime: false} # {:dep_from_hexpm, "~> 0.3.0"}, # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} ] diff --git a/test/mix/tasks/simple_blog/compile_test.exs b/test/mix/tasks/simple_blog/compile_test.exs new file mode 100644 index 0000000..9768da3 --- /dev/null +++ b/test/mix/tasks/simple_blog/compile_test.exs @@ -0,0 +1,43 @@ +defmodule Mix.Tasks.SimpleBlog.CompileTest do + use ExUnit.Case + + describe "run/1" do + setup do + on_exit(fn -> File.rm_rf("output_test") end) + end + + test "create a directory to static blog" do + Mix.Tasks.SimpleBlog.Compile.run(["blog_test", "output_test"]) + assert File.exists?("output_test") + end + + test "converts posts to html" do + Mix.Tasks.SimpleBlog.Post.run(["My First Blog Post", "blog_test"]) + Mix.Tasks.SimpleBlog.Compile.run(["blog_test", "output_test"]) + + today = Date.utc_today() |> Date.to_string() + dir = SimpleBlog.Post.generate_html_dir(%SimpleBlog.Post{date: today}, "output_test/posts") + + assert File.exists?(dir <> "my-first-blog-post.html") + + on_exit(fn -> File.rm("blog_test/_posts/#{today}-my-first-blog-post.md") end) + end + + test "creates css files" do + Mix.Tasks.SimpleBlog.Compile.run(["blog_test", "output_test"]) + css_dir = "output_test/css/" + + assert File.exists?(css_dir <> "_solarized-light.css") + assert File.exists?(css_dir <> "plain.css") + assert File.exists?(css_dir <> "reset.css") + assert File.exists?(css_dir <> "style.css") + end + + test "creates images files" do + Mix.Tasks.SimpleBlog.Compile.run(["blog_test", "output_test"]) + images_dir = "output_test/images/" + + assert File.exists?(images_dir <> "avatar.png") + end + end +end diff --git a/test/mix/tasks/simple_blog/post/new_test.exs b/test/mix/tasks/simple_blog/post_test.exs similarity index 50% rename from test/mix/tasks/simple_blog/post/new_test.exs rename to test/mix/tasks/simple_blog/post_test.exs index ffffe1e..a04ff34 100644 --- a/test/mix/tasks/simple_blog/post/new_test.exs +++ b/test/mix/tasks/simple_blog/post_test.exs @@ -1,7 +1,7 @@ -defmodule Mix.Tasks.SimpleBlog.Post.NewTest do +defmodule Mix.Tasks.SimpleBlog.PostTest do use ExUnit.Case import ExUnit.CaptureIO - doctest Mix.Tasks.SimpleBlog.Post.New + # doctest Mix.Tasks.SimpleBlog.Post.New @instructions """ To generate a new blog post you should pass a title as string: @@ -11,13 +11,25 @@ defmodule Mix.Tasks.SimpleBlog.Post.NewTest do describe "run" do test "returns instructions for no arguments" do - message = capture_io(fn -> Mix.Tasks.SimpleBlog.Post.New.run([]) end) + message = capture_io(fn -> Mix.Tasks.SimpleBlog.Post.run([]) end) assert message == "#{@instructions}\n" end test "show success message for created blog post" do - Mix.Tasks.SimpleBlog.Post.New.run(["My First Blog Post", "blog_test"]) + message = + capture_io(fn -> + Mix.Tasks.SimpleBlog.Post.run(["My First Blog Post", "blog_test"]) + end) + + today = Date.utc_today() |> Date.to_string() + + assert message == "Blog post created at blog_test/_posts/#{today}-my-first-blog-post.md\n\n" + on_exit(fn -> File.rm("blog_test/_posts/#{today}-my-first-blog-post.md") end) + end + + test "creates a new markdown file for blog post" do + Mix.Tasks.SimpleBlog.Post.run(["My First Blog Post", "blog_test"]) today = Date.utc_today() |> Date.to_string() @@ -27,7 +39,7 @@ defmodule Mix.Tasks.SimpleBlog.Post.NewTest do test "show error message when blog does not exist" do message = - capture_io(fn -> Mix.Tasks.SimpleBlog.Post.New.run(["My First Blog Post", "invalid"]) end) + capture_io(fn -> Mix.Tasks.SimpleBlog.Post.run(["My First Blog Post", "invalid"]) end) assert message == "The directory invalid was not found\n\n" end @@ -35,7 +47,7 @@ defmodule Mix.Tasks.SimpleBlog.Post.NewTest do describe "usage" do test "returns instructions" do - assert Mix.Tasks.SimpleBlog.Post.New.usage() == @instructions + assert Mix.Tasks.SimpleBlog.Post.usage() == @instructions end end end diff --git a/test/simple_blog/reader/posts_test.exs b/test/simple_blog/reader/posts_test.exs index 880c001..d4800a6 100644 --- a/test/simple_blog/reader/posts_test.exs +++ b/test/simple_blog/reader/posts_test.exs @@ -2,8 +2,8 @@ defmodule SimpleBlog.Reader.PostsTest do use ExUnit.Case setup do - Mix.Tasks.SimpleBlog.Post.New.run(["my first job day", "blog_test"]) - Mix.Tasks.SimpleBlog.Post.New.run(["10 tips for a junior develop", "blog_test"]) + Mix.Tasks.SimpleBlog.Post.run(["my first job day", "blog_test"]) + Mix.Tasks.SimpleBlog.Post.run(["10 tips for a junior develop", "blog_test"]) on_exit(fn -> {:ok, files} = File.ls("blog_test/_posts/") diff --git a/test/simple_blog_test.exs b/test/simple_blog_test.exs deleted file mode 100644 index b0920bc..0000000 --- a/test/simple_blog_test.exs +++ /dev/null @@ -1,8 +0,0 @@ -defmodule SimpleBlogTest do - use ExUnit.Case - doctest SimpleBlog - - test "greets the world" do - assert SimpleBlog.hello() == :world - end -end