diff --git a/lib/outlook/articles.ex b/lib/outlook/articles.ex index 31c9681..1d13105 100644 --- a/lib/outlook/articles.ex +++ b/lib/outlook/articles.ex @@ -6,7 +6,7 @@ defmodule Outlook.Articles do import Ecto.Query, warn: false alias Outlook.Repo - alias Outlook.Articles.Article + alias Outlook.Articles.{Article,RawHtmlInput} @doc """ Returns the list of articles. @@ -101,4 +101,8 @@ defmodule Outlook.Articles do def change_article(%Article{} = article, attrs \\ %{}) do Article.changeset(article, attrs) end + + def change_raw_html_input(%RawHtmlInput{} = raw_html_input, attrs \\ %{}) do + RawHtmlInput.changeset(raw_html_input, attrs) + end end diff --git a/lib/outlook/articles/raw_html_input.ex b/lib/outlook/articles/raw_html_input.ex new file mode 100644 index 0000000..db7dd6b --- /dev/null +++ b/lib/outlook/articles/raw_html_input.ex @@ -0,0 +1,16 @@ +defmodule Outlook.Articles.RawHtmlInput do + use Ecto.Schema + import Ecto.Changeset + + embedded_schema do + field :content, :string + end + + @doc false + def changeset(html_input, attrs) do + html_input + |> cast(attrs, [:content]) + |> validate_required([:content]) + |> validate_length(:content, min: 200) + end +end diff --git a/lib/outlook/html_preparations.ex b/lib/outlook/html_preparations.ex new file mode 100644 index 0000000..cb8c4b5 --- /dev/null +++ b/lib/outlook/html_preparations.ex @@ -0,0 +1,14 @@ +defmodule Outlook.HtmlPreparations do + @moduledoc """ + The HtmlPreparations context. + """ + + alias Outlook.HtmlPreparations.HtmlPreparation + + def convert_raw_html_input(html) do + html + |> Floki.parse_fragment! + |> HtmlPreparation.floki_to_internal + |> HtmlPreparation.set_sibling_with + end +end diff --git a/lib/outlook/html_preparations/html_preparation.ex b/lib/outlook/html_preparations/html_preparation.ex new file mode 100644 index 0000000..1deee75 --- /dev/null +++ b/lib/outlook/html_preparations/html_preparation.ex @@ -0,0 +1,65 @@ +defmodule Outlook.HtmlPreparations.HtmlPreparation do + import Ecto.UUID, only: [generate: 0] + + alias Outlook.InternalTree.InternalNode + + @block_elements ["address","article","aside","blockquote","canvas","dd","div","dl","dt","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hr","li","main","nav","noscript","ol","p","pre","section","table","tfoot","ul","video"] + # @inline_elements ["a","abbr","acronym","b","bdo","big","br","button","cite","code","dfn","em","i","img","input","kbd","label","map","object","output","q","samp","script","select","small","span","strong","sub","sup","textarea","time","tt","u","var"] + + defp clean_atts_to_map(atts) do + atts_to_keep = ~w(href src) + atts_to_rename = ~w(class style src-set) + atts + |> Enum.reject(fn {k,_} -> k not in (atts_to_keep ++ atts_to_rename) end) + |> Enum.reject(fn {_,v} -> v == "" end) + |> Enum.map(fn {k,v} -> {k in atts_to_rename && "#{k}-old" || k, v} end) + |> Enum.map(fn {k,v} -> {String.to_atom(k),v} end) + |> Enum.into(%{}) + end + + def floki_to_internal [ { tag, attributes, content } | rest ] do + [ %InternalNode{ + name: tag, + attributes: clean_atts_to_map(attributes), + type: :element, + uuid: generate(), + content: floki_to_internal(content) + } | floki_to_internal(rest) ] + end + + def floki_to_internal [ "" <> textnode | rest ] do + [ %InternalNode{ + type: :text, + uuid: generate(), + content: textnode + } | floki_to_internal(rest) ] + end + + def floki_to_internal [ {:comment, comment} | rest ] do + [ %InternalNode{ + type: :comment, + uuid: generate(), + content: comment + } | floki_to_internal(rest) ] + end + + def floki_to_internal([ ]), do: ( [ ] ) + + + def set_sibling_with([ %{type: :element} = node | rest ]) do + [ %InternalNode{ node | + sibling_with: node.name in @block_elements && :block || :inline, + content: set_sibling_with(node.content) + } | set_sibling_with(rest) ] + end + + def set_sibling_with([ node | rest ]) do + sib_with = case node.type do + :text -> Regex.match?(~r/^\s*$/, node.content) && :both || :inline + :comment -> :both + end + [ %InternalNode{ node | sibling_with: sib_with } | set_sibling_with(rest) ] + end + + def set_sibling_with([ ]), do: ( [ ] ) +end diff --git a/lib/outlook/internal_tree/internal_node.ex b/lib/outlook/internal_tree/internal_node.ex new file mode 100644 index 0000000..3a16c5e --- /dev/null +++ b/lib/outlook/internal_tree/internal_node.ex @@ -0,0 +1,4 @@ +defmodule Outlook.InternalTree.InternalNode do + @derive Jason.Encoder + defstruct name: "", attributes: %{}, type: :atom, uuid: "", content: [], sibling_with: nil +end diff --git a/lib/outlook_web/live/article_live/index.html.heex b/lib/outlook_web/live/article_live/index.html.heex index 61a6418..84704e2 100644 --- a/lib/outlook_web/live/article_live/index.html.heex +++ b/lib/outlook_web/live/article_live/index.html.heex @@ -1,9 +1,6 @@ <.header> Listing Articles <:actions> - <.link patch={~p"/articles/new"}> - <.button>New Article - diff --git a/lib/outlook_web/live/article_live/new.ex b/lib/outlook_web/live/article_live/new.ex new file mode 100644 index 0000000..5d29f1b --- /dev/null +++ b/lib/outlook_web/live/article_live/new.ex @@ -0,0 +1,55 @@ +defmodule OutlookWeb.ArticleLive.New do + use OutlookWeb, :live_view + + import OutlookWeb.ArticleLive.NewComponents + + alias Outlook.{Articles,Authors,HtmlPreparations} + alias Articles.{Article,RawHtmlInput} + + require Logger + + @impl true + def mount(_params, _session, socket) do + {:ok, + socket + |> assign(:page_title, "New Article") + |> assign(:article, %Article{}) + |> assign(:raw_html_input, %RawHtmlInput{}) + |> assign(:changeset, Articles.change_raw_html_input(%RawHtmlInput{})) + |> assign(:selected_els, []) + |> assign(:step, :import_raw_html)} + end + + @impl true + def handle_params(%{"author_id" => author_id}, _, socket) do + author = Authors.get_author!(author_id) + {:noreply, + socket + |> assign(:author, author)} + end + + @impl true + def handle_event("validate_raw_html_input", %{"raw_html_input" => raw_html_input_params}, socket) do + changeset = validate_raw_html_input(raw_html_input_params, socket) + {:noreply, assign(socket, :changeset, changeset)} + end + + def handle_event("convert_raw_html_input", %{"raw_html_input" => raw_html_input_params}, socket) do + changeset = validate_raw_html_input(raw_html_input_params, socket) + case changeset.valid? do + true -> + {:noreply, + socket + |> assign(:raw_internal_tree, HtmlPreparations.convert_raw_html_input(raw_html_input_params["content"])) + |> assign(:step, :review_raw_internaltree)} + false -> + {:noreply, assign(socket, :changeset, changeset)} + end + end + + defp validate_raw_html_input(raw_html_input_params, socket) do + socket.assigns.raw_html_input + |> Articles.change_raw_html_input(raw_html_input_params) + |> Map.put(:action, :validate) + end +end diff --git a/lib/outlook_web/live/article_live/new.html.heex b/lib/outlook_web/live/article_live/new.html.heex new file mode 100644 index 0000000..f3ba423 --- /dev/null +++ b/lib/outlook_web/live/article_live/new.html.heex @@ -0,0 +1,9 @@ +<.header> + New article for <%= @author.name %> + + +<.import_raw_html :if={@step == :import_raw_html} changeset={@changeset}> +<.review_raw_internaltree :if={@step == :review_raw_internaltree}> +<.review_translation_units :if={@step == :review_translation_units}> + +<.back navigate={~p"/authors/#{@author}"}>Back to author diff --git a/lib/outlook_web/live/article_live/new_components.ex b/lib/outlook_web/live/article_live/new_components.ex new file mode 100644 index 0000000..231fde6 --- /dev/null +++ b/lib/outlook_web/live/article_live/new_components.ex @@ -0,0 +1,37 @@ +defmodule OutlookWeb.ArticleLive.NewComponents do + use OutlookWeb, :html + + def import_raw_html(assigns) do + ~H""" +
+
Import article
+ + <.simple_form + :let={f} + for={@changeset} + id="raw-html-input-form" + phx-change="validate_raw_html_input" + phx-submit="convert_raw_html_input" + > + <.input field={{f, :content}} type="textarea" label="text to import" phx-debounce="500" /> + <:actions> + <.button phx-disable-with="Importing...">HTML importieren + + +
+ """ + end + + def review_raw_internaltree(assigns) do + ~H""" +
Review Raw InternalTree
+ + """ + end + + def review_translation_units(assigns) do + ~H""" +
Review Translation Units
+ """ + end +end diff --git a/lib/outlook_web/live/author_live/show.html.heex b/lib/outlook_web/live/author_live/show.html.heex index 3429bd0..9c70c74 100644 --- a/lib/outlook_web/live/author_live/show.html.heex +++ b/lib/outlook_web/live/author_live/show.html.heex @@ -5,6 +5,9 @@ <.link patch={~p"/authors/#{@author}/show/edit"} phx-click={JS.push_focus()}> <.button>Edit author + <.link patch={~p"/articles/new?author_id=#{@author}"} phx-click={JS.push_focus()}> + <.button>New article + diff --git a/lib/outlook_web/router.ex b/lib/outlook_web/router.ex index 673010d..c37de82 100644 --- a/lib/outlook_web/router.ex +++ b/lib/outlook_web/router.ex @@ -78,9 +78,10 @@ defmodule OutlookWeb.Router do live "/authors/:id/show/edit", AuthorLive.Show, :edit live "/articles", ArticleLive.Index, :index - live "/articles/new", ArticleLive.Index, :new live "/articles/:id/edit", ArticleLive.Index, :edit + live "/articles/new", ArticleLive.New, :new + live "/articles/:id", ArticleLive.Show, :show live "/articles/:id/show/edit", ArticleLive.Show, :edit diff --git a/mix.exs b/mix.exs index 748cd9b..4271dc9 100644 --- a/mix.exs +++ b/mix.exs @@ -41,7 +41,7 @@ defmodule Outlook.MixProject do {:phoenix_live_reload, "~> 1.2", only: :dev}, {:phoenix_live_view, "~> 0.18.3"}, {:heroicons, "~> 0.5"}, - {:floki, ">= 0.30.0", only: :test}, + {:floki, ">= 0.30.0"}, {:phoenix_live_dashboard, "~> 0.7.2"}, {:esbuild, "~> 0.5", runtime: Mix.env() == :dev}, {:tailwind, "~> 0.1.8", runtime: Mix.env() == :dev},