diff --git a/lib/outlook/articles.ex b/lib/outlook/articles.ex index 1d13105..999f493 100644 --- a/lib/outlook/articles.ex +++ b/lib/outlook/articles.ex @@ -35,7 +35,10 @@ defmodule Outlook.Articles do ** (Ecto.NoResultsError) """ - def get_article!(id), do: Repo.get!(Article, id) + def get_article!(id) do + Repo.get!(Article, id) + |> Repo.preload([:author]) + end @doc """ Creates a article. diff --git a/lib/outlook/articles/article.ex b/lib/outlook/articles/article.ex index 1c4a25c..86e7f9c 100644 --- a/lib/outlook/articles/article.ex +++ b/lib/outlook/articles/article.ex @@ -2,11 +2,12 @@ defmodule Outlook.Articles.Article do use Ecto.Schema import Ecto.Changeset + alias Outlook.Articles.InternalTree alias Outlook.Authors.Author alias Outlook.Translations.Translation schema "articles" do - field :content, :string + field :content, InternalTree field :date, :utc_datetime field :language, :string field :title, :string @@ -20,7 +21,7 @@ defmodule Outlook.Articles.Article do @doc false def changeset(article, attrs) do article - |> cast(attrs, [:title, :content, :url, :language, :date]) - |> validate_required([:title, :content, :url, :language, :date]) + |> cast(attrs, [:title, :content, :url, :language, :date, :author_id]) + |> validate_required([:title, :content, :url, :language, :date, :author_id]) end end diff --git a/lib/outlook/articles/internal_tree.ex b/lib/outlook/articles/internal_tree.ex new file mode 100644 index 0000000..fedfce0 --- /dev/null +++ b/lib/outlook/articles/internal_tree.ex @@ -0,0 +1,52 @@ +defmodule Outlook.Articles.InternalTree do + use Ecto.Type + + alias Outlook.InternalTree.InternalNode + alias Outlook.InternalTree.TranslationUnit + + def type, do: :string + + def cast(tree) when is_list(tree) do + {:ok, tree} + end + + def cast(_), do: :error + + def load(tree) when is_binary(tree) do + {:ok, Jason.decode!(tree, keys: :atoms!) |> from_json} + end + + def dump(tree) when is_list(tree), do: {:ok, Jason.encode!(tree)} + def dump(_), do: :error + + + defp from_json([%{status: _} = node | rest]) do + [ %TranslationUnit{ + status: String.to_atom(node.status), + uuid: node.uuid, + content: node.content + } | from_json(rest) ] + end + + defp from_json([%{type: "element"} = node | rest]) do + [ %InternalNode{ + name: node.name, + attributes: node.attributes, + type: String.to_atom(node.type), + uuid: node.uuid, + content: from_json(node.content) + } | from_json(rest) ] + end + + defp from_json([%{type: _} = node | rest]) do + [ %InternalNode{ + name: node.name, + attributes: node.attributes, + type: String.to_atom(node.type), + uuid: node.uuid, + content: node.content + } | from_json(rest) ] + end + + defp from_json([]), do: [] +end diff --git a/lib/outlook/authors.ex b/lib/outlook/authors.ex index 180b7b0..6edb2e1 100644 --- a/lib/outlook/authors.ex +++ b/lib/outlook/authors.ex @@ -37,6 +37,11 @@ defmodule Outlook.Authors do """ def get_author!(id), do: Repo.get!(Author, id) + def get_author_with_articles!(id) do + Repo.get!(Author, id) + |> Repo.preload([:articles]) + end + @doc """ Creates a author. diff --git a/lib/outlook/authors/author.ex b/lib/outlook/authors/author.ex index 626f8d8..704f5bb 100644 --- a/lib/outlook/authors/author.ex +++ b/lib/outlook/authors/author.ex @@ -18,6 +18,6 @@ defmodule Outlook.Authors.Author do def changeset(author, attrs) do author |> cast(attrs, [:name, :description, :homepage_name, :homepage_url]) - |> validate_required([:name, :description, :homepage_name, :homepage_url]) + |> validate_required([:name, :description]) end end diff --git a/lib/outlook/html_preparations.ex b/lib/outlook/html_preparations.ex index cb8c4b5..864a1ba 100644 --- a/lib/outlook/html_preparations.ex +++ b/lib/outlook/html_preparations.ex @@ -11,4 +11,10 @@ defmodule Outlook.HtmlPreparations do |> HtmlPreparation.floki_to_internal |> HtmlPreparation.set_sibling_with end + + def get_tree_items(content_tree) do + content_tree + |> HtmlPreparation.strip_whitespace_textnodes + |> HtmlPreparation.build_indentation_list(0) + end end diff --git a/lib/outlook/html_preparations/html_preparation.ex b/lib/outlook/html_preparations/html_preparation.ex index 1deee75..d8013f4 100644 --- a/lib/outlook/html_preparations/html_preparation.ex +++ b/lib/outlook/html_preparations/html_preparation.ex @@ -62,4 +62,40 @@ defmodule Outlook.HtmlPreparations.HtmlPreparation do end def set_sibling_with([ ]), do: ( [ ] ) + + def strip_whitespace_textnodes [ %{type: :text} = node | rest] do + if Regex.match?(~r/^\s*$/, node.content) do + strip_whitespace_textnodes(rest) + else + [ node | strip_whitespace_textnodes(rest)] + end + end + + + def strip_whitespace_textnodes [ %{type: :element} = node | rest] do + [ %InternalNode{ node | content: strip_whitespace_textnodes(node.content) } + | strip_whitespace_textnodes(rest) ] + end + + def strip_whitespace_textnodes [ node | rest] do + [ node | strip_whitespace_textnodes(rest) ] + end + + def strip_whitespace_textnodes([]), do: [] + + + def build_indentation_list [ %{type: :element} = node | rest], level do + [ %{node: Map.replace(node, :content, []), level: level} + | [ build_indentation_list(node.content, level + 1) + | build_indentation_list(rest, level) + ] + ] |> List.flatten + end + + def build_indentation_list [ node | rest ], level do + [ %{node: node, level: level} + | build_indentation_list( rest, level ) ] + end + + def build_indentation_list([ ], _), do: [] end diff --git a/lib/outlook/internal_tree.ex b/lib/outlook/internal_tree.ex new file mode 100644 index 0000000..ac15018 --- /dev/null +++ b/lib/outlook/internal_tree.ex @@ -0,0 +1,30 @@ +defmodule Outlook.InternalTree do + + alias Outlook.InternalTree.{Html,Modifiers,Basic} + alias Outlook.HtmlPreparations.HtmlPreparation + + def render_html(tree) do + tree + |> HtmlPreparation.strip_whitespace_textnodes() + |> Html.to_html() + end + + def render_html_preview(tree) do + tree + |> partition_text + |> Html.to_html_preview("1") + end + + require Logger + def apply_modifier(tree, modifier, uuids, opts \\ %{}) do + # Logger.info modifier + Modifiers.traverse_tree(tree, modifier, uuids, opts) + end + + def partition_text(tree) do + # validate_sibling_collocation(tree) + tree + |> Basic.set_split_markers() + |> Basic.partition_textnodes() + end +end diff --git a/lib/outlook/internal_tree/html.ex b/lib/outlook/internal_tree/html.ex new file mode 100644 index 0000000..ab33aaa --- /dev/null +++ b/lib/outlook/internal_tree/html.ex @@ -0,0 +1,52 @@ +defmodule Outlook.InternalTree.Html do + + alias Outlook.InternalTree.InternalNode + alias Outlook.InternalTree.TranslationUnit + + def to_html([ %InternalNode{type: :element} = node | rest]) do + attr_string = Map.put(node.attributes, :uuid, node.uuid) + |> Enum.map_join(" ", fn {k,v} -> "#{k}=\"#{v}\"" end) + "<#{node.name} #{attr_string}>" <> + to_html(node.content) <> + "#{node.name}>" <> + to_html(rest) + end + + def to_html([ %InternalNode{type: :text} = node | rest]) do + node.content <> to_html(rest) + end + + def to_html([ %InternalNode{type: :comment} = node | rest]) do + "" <> to_html(rest) + end + + def to_html([ %TranslationUnit{} = tunit | rest]) do + ~s(#{tunit.content}) <> to_html(rest) + end + + def to_html([]), do: "" + + def to_html_preview([ %InternalNode{type: :element} = node | rest], target_id) do + attr_string = Map.put(node.attributes, :uuid, node.uuid) + |> Enum.map_join(" ", fn {k,v} -> "#{k}=\"#{v}\"" end) + "<#{node.name} #{attr_string}>" <> + to_html_preview(node.content, target_id) <> + "#{node.name}>" <> + to_html_preview(rest, target_id) + end + + def to_html_preview([ %InternalNode{type: :text} = node | rest], target_id) do + ~s(#{node.content}) <> to_html_preview(rest, target_id) + end + + def to_html_preview([ %InternalNode{type: :comment} = node | rest], target_id) do + ~s() <> to_html_preview(rest, target_id) + end + + def to_html_preview([ %TranslationUnit{} = tunit | rest], target_id) do + ~s|#{tunit.content}| <> to_html_preview(rest, target_id) + end + + def to_html_preview([], _target_id), do: "" +end diff --git a/lib/outlook/internal_tree/internal_tree_basic.ex b/lib/outlook/internal_tree/internal_tree_basic.ex new file mode 100644 index 0000000..c844fde --- /dev/null +++ b/lib/outlook/internal_tree/internal_tree_basic.ex @@ -0,0 +1,70 @@ +defmodule Outlook.InternalTree.Basic do + + alias Ecto.UUID + alias Outlook.InternalTree.InternalNode + alias Outlook.InternalTree.TranslationUnit + alias Outlook.InternalTree.Html + + @splitmarker "@@translationunit@@" + + def set_split_markers([ %InternalNode{type: :text} = textnode | rest ]) do + [ %InternalNode{textnode | + content: String.replace(textnode.content, ~r|([.?!]["'”]?\s*)|u, "\\1#{@splitmarker}") + } | set_split_markers(rest) ] + end + + def set_split_markers([ %InternalNode{type: :element} = node | rest ]) do + [ %InternalNode{node | content: set_split_markers(node.content)} + | set_split_markers(rest) ] + end + + def set_split_markers([ node | rest ]) do + [ node | set_split_markers(rest) ] + end + + def set_split_markers([]), do: [] + + def partition_textnodes([ %InternalNode{type: :element} = node | rest ]) do + [ %InternalNode{node | content: case get_sibling_collocation(node.content) do + :block -> partition_textnodes(node.content) + :inline -> inline_to_translation_units(node.content) + _ -> node # rare case of only whitespace textnode(s) - does this ever happen? + end + } | partition_textnodes(rest) ] + end + + def partition_textnodes([ node | rest ]) do + [ node | partition_textnodes(rest) ] + end + + def partition_textnodes([]), do: [] + + + defp inline_to_translation_units(contents) do + contents + # |> Html.strip_attributes # to be implemented + |> Html.to_html() + |> String.split(@splitmarker, trim: true) + |> Enum.map(fn sentence -> + %TranslationUnit{ + content: sentence, + status: :untranslated, + uuid: UUID.generate() + } + end + ) + end + + defp contains_elements?(content) do + + end + + @doc "Returns just either :block or :inline. Assumes that it doesn't contain both." + def get_sibling_collocation(content) do + content + |> Enum.map(fn node -> node.sibling_with end) + |> Enum.uniq() + |> List.delete(:both) + |> List.first + end +end diff --git a/lib/outlook/internal_tree/raw_internal_tree.ex b/lib/outlook/internal_tree/raw_internal_tree.ex new file mode 100644 index 0000000..c693856 --- /dev/null +++ b/lib/outlook/internal_tree/raw_internal_tree.ex @@ -0,0 +1,19 @@ +defmodule Outlook.Articles.RawInternalTree do + use Ecto.Type + + def type, do: :string + + def cast([] = tree) do + {:ok, tree} + end + + def cast(_), do: :error + + def load(tree) when is_binary(tree) do + {:ok, Jason.decode!(tree, keys: :atoms!)} + end + + def dump([] = tree ), do: {:ok, Jason.encode!(tree)} + def dump(_), do: :error + +end diff --git a/lib/outlook/internal_tree/raw_internal_tree_schema.ex b/lib/outlook/internal_tree/raw_internal_tree_schema.ex new file mode 100644 index 0000000..8ae9d4b --- /dev/null +++ b/lib/outlook/internal_tree/raw_internal_tree_schema.ex @@ -0,0 +1,29 @@ +defmodule Outlook.Articles.RawInternalTreeSchema do + use Ecto.Schema + import Ecto.Changeset + + alias Outlook.Articles.RawInternalTree + + embedded_schema do + field :tree, RawInternalTree + end + + @doc false + def changeset(raw_tree, attrs) do + raw_tree + |> check_sibling_collocation(:tree) + end + + def check_sibling_collocation(changeset, field) when is_atom(field) do + validate_change(changeset, field, fn field, value -> + case value do + true -> + [] + + false -> + [{field, "html should be useful"}] + end + end) + end + +end diff --git a/lib/outlook/internal_tree/translation_unit.ex b/lib/outlook/internal_tree/translation_unit.ex new file mode 100644 index 0000000..e85e564 --- /dev/null +++ b/lib/outlook/internal_tree/translation_unit.ex @@ -0,0 +1,4 @@ +defmodule Outlook.InternalTree.TranslationUnit do + @derive Jason.Encoder + defstruct status: :atom, uuid: "", content: "" +end diff --git a/lib/outlook_web.ex b/lib/outlook_web.ex index 35a8d97..1c7a24a 100644 --- a/lib/outlook_web.ex +++ b/lib/outlook_web.ex @@ -86,6 +86,7 @@ defmodule OutlookWeb do import Phoenix.HTML # Core UI components and translation import OutlookWeb.CoreComponents + import OutlookWeb.HtmlTreeComponent import OutlookWeb.Gettext # Shortcut for generating JS commands diff --git a/lib/outlook_web/components/html_tree_component.ex b/lib/outlook_web/components/html_tree_component.ex new file mode 100644 index 0000000..79574fc --- /dev/null +++ b/lib/outlook_web/components/html_tree_component.ex @@ -0,0 +1,57 @@ +defmodule OutlookWeb.HtmlTreeComponent do + + use Phoenix.Component + # use OutlookWeb, :html + import OutlookWeb.CoreComponents + + alias Phoenix.LiveView.JS + + attr :tree_items, :list, required: true + + def treeview(assigns) do + ~H""" +