diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..1e582b0 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "mds-converter"] + path = mds-converter + url = git@github.com:markdown-docs/mds-converter.git diff --git a/lib/backend/application.ex b/lib/backend/application.ex index 5ef4648..a6571aa 100644 --- a/lib/backend/application.ex +++ b/lib/backend/application.ex @@ -19,7 +19,10 @@ defmodule Backend.Application do # Start to serve requests, typically the last entry BackendWeb.Endpoint, Backend.Files.FileRegistry, - Backend.Files.FileManager + Backend.Files.FileManager, + # Backend.Parsers.Pool + # Backend.Parsers.MdsParser + {Task.Supervisor, name: Backend.TaskSupervisor} ] # See https://hexdocs.pm/elixir/Supervisor.html diff --git a/lib/backend/files/files.ex b/lib/backend/files/files.ex index 9e88272..34b2ae4 100644 --- a/lib/backend/files/files.ex +++ b/lib/backend/files/files.ex @@ -9,6 +9,10 @@ defmodule Backend.Files do Repo.get(File, id) end + def get_files() do + Repo.all(File) + end + def create_file(attrs \\ %{}) do %File{} |> File.changeset(attrs) diff --git a/lib/backend/parsers/mds_parser.ex b/lib/backend/parsers/mds_parser.ex new file mode 100644 index 0000000..1151c40 --- /dev/null +++ b/lib/backend/parsers/mds_parser.ex @@ -0,0 +1,16 @@ +defmodule Backend.Parsers.MdsParser do + @moduledoc """ + MdsParser used to invoke haskell parser + """ + + @parser_path Path.expand("mds-converter/runnable/parser", File.cwd!()) + + def parse(markdown) do + args = ["-c", markdown] + + case System.cmd(@parser_path, args) do + {output, 0} -> {:ok, output} + {error, status} -> {:error, "Process failed with status #{status}: #{error}"} + end + end +end diff --git a/lib/backend_web/channels/file_channel.ex b/lib/backend_web/channels/file_channel.ex new file mode 100644 index 0000000..344c0bc --- /dev/null +++ b/lib/backend_web/channels/file_channel.ex @@ -0,0 +1,46 @@ +defmodule BackendWeb.FileChannel do + @moduledoc """ + FileChannel used to interact with file's content + """ + + use Phoenix.Channel + + alias Backend.Files + alias Backend.Files.FileManager + alias Backend.Parsers.MdsParser + alias BackendWeb.PreviewChannel + + require Logger + + def join("file:" <> file_id, _params, socket) do + case Files.get_file(file_id) do + nil -> {:error, %{reason: "File with id " <> file_id <> " does not exist"}} + _ -> {:ok, assign(socket, :file_id, file_id)} + end + end + + def handle_in("edit", %{"content" => content}, socket) do + file_id = socket.assigns.file_id + + Logger.debug("HANDLED edit INCOMING ON file:#{file_id}") + Logger.debug(" Parameters: #{inspect(%{"content" => content})}") + + FileManager.update_file_content(file_id, content) + broadcast!(socket, "update", %{content: content}) + + Task.Supervisor.start_child(Backend.TaskSupervisor, fn -> + case MdsParser.parse(content) do + {:ok, html} -> + PreviewChannel.send_preview(file_id, html) + Logger.debug("SENT preview BACK ON preview:#{file_id}") + # Limit log size + Logger.debug(" HTML Content: #{String.slice(html, 0..100)}...") + + {:error, reason} -> + Logger.error("Parser error: #{inspect(reason)}") + end + end) + + {:noreply, socket} + end +end diff --git a/lib/backend_web/channels/preview_channel.ex b/lib/backend_web/channels/preview_channel.ex new file mode 100644 index 0000000..3d5f76b --- /dev/null +++ b/lib/backend_web/channels/preview_channel.ex @@ -0,0 +1,23 @@ +defmodule BackendWeb.PreviewChannel do + @moduledoc """ + Websocket channel for each **converted** .mds file + """ + use Phoenix.Channel + + def join("preview:" <> file_id, _params, socket) do + # store process PID in ETS (Erlang Term Storage) to listen to messages in topic + Phoenix.PubSub.subscribe(Backend.PubSub, "preview:#{file_id}") + {:ok, assign(socket, :file_id, file_id)} + end + + # Отправка готового превью + def send_preview(file_id, html) do + # find all subscribed to preview topic procs in ETS (Erlang Term Storage) and send them message + Phoenix.PubSub.broadcast(Backend.PubSub, "preview:#{file_id}", {:preview, html}) + end + + def handle_info({:preview, html}, socket) do + push(socket, "preview", %{html: html}) + {:noreply, socket} + end +end diff --git a/lib/backend_web/channels/room_channel.ex b/lib/backend_web/channels/room_channel.ex deleted file mode 100644 index 02861a8..0000000 --- a/lib/backend_web/channels/room_channel.ex +++ /dev/null @@ -1,27 +0,0 @@ -defmodule BackendWeb.FileChannel do - @moduledoc """ - Websocket channel for each .mds file - """ - alias Backend.Files - alias Backend.Files.FileManager - use Phoenix.Channel - - def join("file:" <> file_id, _params, socket) do - case Files.get_file(file_id) do - nil -> {:error, %{reason: "File with id " <> file_id <> " does not exist"}} - _ -> {:ok, assign(socket, :file_id, file_id)} - end - end - - def handle_in("edit", %{"content" => content}, socket) do - # Получаем file_id из socket.assigns - file_id = socket.assigns.file_id - - # Вызываем FileManager для обновления содержимого файла - FileManager.update_file_content(file_id, content) - - # Рассылаем обновления всем клиентам - broadcast!(socket, "update", %{content: content}) - {:noreply, socket} - end -end diff --git a/lib/backend_web/channels/user_socket.ex b/lib/backend_web/channels/user_socket.ex index 08151fe..0074892 100644 --- a/lib/backend_web/channels/user_socket.ex +++ b/lib/backend_web/channels/user_socket.ex @@ -2,8 +2,8 @@ defmodule BackendWeb.UserSocket do use Phoenix.Socket ## Каналы - # Это маршрут, на который будет подписан клиент channel "file:*", BackendWeb.FileChannel + channel "preview:*", BackendWeb.PreviewChannel # Здесь можно добавить авторизацию (опционально) def connect(_params, socket, _connect_info) do diff --git a/lib/backend_web/controllers/file_controller.ex b/lib/backend_web/controllers/file_controller.ex index 4d97003..6ed6704 100644 --- a/lib/backend_web/controllers/file_controller.ex +++ b/lib/backend_web/controllers/file_controller.ex @@ -18,6 +18,14 @@ defmodule BackendWeb.FileController do end end + def get_files(conn, _param) do + files = Files.get_files() + + conn + |> put_status(200) + |> json(files) + end + def create_file(conn, %{"name" => name}) do case Files.create_file(%{"name" => name, "content" => ""}) do {:ok, file} -> diff --git a/lib/backend_web/endpoint.ex b/lib/backend_web/endpoint.ex index 5728263..7a86dac 100644 --- a/lib/backend_web/endpoint.ex +++ b/lib/backend_web/endpoint.ex @@ -42,6 +42,7 @@ defmodule BackendWeb.Endpoint do param_key: "request_logger", cookie_key: "request_logger" + plug CORSPlug plug Plug.RequestId plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] diff --git a/lib/backend_web/router.ex b/lib/backend_web/router.ex index 70eb75f..0705423 100644 --- a/lib/backend_web/router.ex +++ b/lib/backend_web/router.ex @@ -20,6 +20,7 @@ defmodule BackendWeb.Router do get "/", PageController, :home get "/file/:id", FileController, :get_file + get "/file", FileController, :get_files post "/file", FileController, :create_file delete "/file/:id", FileController, :delete_file end diff --git a/mds-converter b/mds-converter new file mode 160000 index 0000000..d6a5414 --- /dev/null +++ b/mds-converter @@ -0,0 +1 @@ +Subproject commit d6a5414bd294df9fec3bdec454f5299f3579c6f6 diff --git a/mix.exs b/mix.exs index a14294c..dca6ee1 100644 --- a/mix.exs +++ b/mix.exs @@ -68,7 +68,9 @@ defmodule Backend.MixProject do {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, {:propcheck, "~> 1.4.1", only: [:test]}, {:excoveralls, "~> 0.18", only: :test}, - {:ecto_sqlite3, "~> 0.10.0"} + {:ecto_sqlite3, "~> 0.10.0"}, + {:mock, "~> 0.3.8", only: :test}, + {:cors_plug, "~> 3.0"} ] end diff --git a/mix.lock b/mix.lock index ea856cf..3cc520c 100644 --- a/mix.lock +++ b/mix.lock @@ -3,6 +3,7 @@ "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "castore": {:hex, :castore, "1.0.10", "43bbeeac820f16c89f79721af1b3e092399b3a1ecc8df1a472738fd853574911", [:mix], [], "hexpm", "1b0b7ea14d889d9ea21202c43a4fa015eb913021cb535e8ed91946f4b77a8848"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"}, + "cors_plug": {:hex, :cors_plug, "3.0.3", "7c3ac52b39624bc616db2e937c282f3f623f25f8d550068b6710e58d04a0e330", [:mix], [{:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3f2d759e8c272ed3835fab2ef11b46bddab8c1ab9528167bd463b6452edf830d"}, "credo": {:hex, :credo, "1.7.10", "6e64fe59be8da5e30a1b96273b247b5cf1cc9e336b5fd66302a64b25749ad44d", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "71fbc9a6b8be21d993deca85bf151df023a3097b01e09a2809d460348561d8cd"}, "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, "decimal": {:hex, :decimal, "2.2.0", "df3d06bb9517e302b1bd265c1e7f16cda51547ad9d99892049340841f3e15836", [:mix], [], "hexpm", "af8daf87384b51b7e611fb1a1f2c4d4876b65ef968fa8bd3adf44cff401c7f21"}, @@ -23,8 +24,10 @@ "hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"}, + "meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"}, "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, "mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"}, + "mock": {:hex, :mock, "0.3.8", "7046a306b71db2488ef54395eeb74df0a7f335a7caca4a3d3875d1fc81c884dd", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "7fa82364c97617d79bb7d15571193fc0c4fe5afd0c932cef09426b3ee6fe2022"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, "phoenix": {:hex, :phoenix, "1.7.14", "a7d0b3f1bc95987044ddada111e77bd7f75646a08518942c72a8440278ae7825", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"}, @@ -37,6 +40,7 @@ "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, + "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, "postgrex": {:hex, :postgrex, "0.19.3", "a0bda6e3bc75ec07fca5b0a89bffd242ca209a4822a9533e7d3e84ee80707e19", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "d31c28053655b78f47f948c85bb1cf86a9c1f8ead346ba1aa0d0df017fa05b61"}, "propcheck": {:hex, :propcheck, "1.4.1", "c12908dbe6f572032928548089b34ff9d40672d5d70f1562e3a9e9058d226cc9", [:mix], [{:libgraph, "~> 0.13", [hex: :libgraph, repo: "hexpm", optional: false]}, {:proper, "~> 1.4", [hex: :proper, repo: "hexpm", optional: false]}], "hexpm", "e1b088f574785c3c7e864da16f39082d5599b3aaf89086d3f9be6adb54464b19"}, "proper": {:hex, :proper, "1.4.0", "89a44b8c39d28bb9b4be8e4d715d534905b325470f2e0ec5e004d12484a79434", [:rebar3], [], "hexpm", "18285842185bd33efbda97d134a5cb5a0884384db36119fee0e3cfa488568cbb"}, diff --git a/priv/repo/migrations/20241129212656_add_file.exs b/priv/repo/migrations/20241129212656_add_file.exs index ec46f1c..d809bf3 100644 --- a/priv/repo/migrations/20241129212656_add_file.exs +++ b/priv/repo/migrations/20241129212656_add_file.exs @@ -4,7 +4,7 @@ defmodule Backend.Repo.Migrations.AddFile do def change do create table(:files) do add :name, :string, null: false - add :content, :string + add :content, :string, size: 10000 end end end diff --git a/test/backend/mds_parser_test.exs b/test/backend/mds_parser_test.exs new file mode 100644 index 0000000..8892689 --- /dev/null +++ b/test/backend/mds_parser_test.exs @@ -0,0 +1,38 @@ +defmodule Backend.Parsers.MdsParserTest do + use ExUnit.Case, async: true + + alias Backend.Parsers.MdsParser + + @parser_path Path.expand("mds-converter/runnable/parser", File.cwd!()) + + setup do + # Проверяем, существует ли парсер + if File.exists?(@parser_path) do + :ok + else + {:error, "Parser executable not found at #{@parser_path}"} + end + end + + test "parses valid content" do + content = "# Test\n\nSome **bold** text" + assert {:ok, html} = MdsParser.parse(content) + + assert html =~ + "\n\n\n\n\n\n

Test

\n

Some bold text

\n\n\n\n" + end + + test "handles invalid content gracefully" do + content = "" + assert {:ok, html} = MdsParser.parse(content) + + # Даже пустой контент должен возвращать корректный HTML + assert html =~ "" + end + + test "handles parser errors" do + # Очень длинный ввод для потенциальной ошибки + content = :binary.copy("a", 1_000_000) + assert {:error, _} = MdsParser.parse(content) + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs index 9cb37c0..06ec5e7 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -5,4 +5,7 @@ Mix.Task.run("ecto.create", ["--quiet"]) Mix.Task.run("ecto.migrate", ["--quiet"]) ExUnit.start() +# Code.require_file("support/channel_case.ex", __DIR__) +Code.require_file("support/data_case.ex", __DIR__) +Code.require_file("support/conn_case.ex", __DIR__) Ecto.Adapters.SQL.Sandbox.mode(Backend.Repo, :manual)