diff --git a/lib/phoenix_kit/blogging/renderer.ex b/lib/phoenix_kit/blogging/renderer.ex index 4283e054a..7e6be3286 100644 --- a/lib/phoenix_kit/blogging/renderer.ex +++ b/lib/phoenix_kit/blogging/renderer.ex @@ -16,6 +16,7 @@ defmodule PhoenixKit.Blogging.Renderer do @cache_name :blog_posts @cache_version "v1" @component_regex ~r/<(Image|Hero|CTA|Headline|Subheadline|Video)\s+([^>]*?)\/>/s + @component_block_regex ~r/<(Hero|CTA|Headline|Subheadline|Video)\s*([^>]*)>(.*?)<\/\1>/s @doc """ Renders a post's markdown content to HTML. @@ -86,8 +87,11 @@ defmodule PhoenixKit.Blogging.Renderer do # Detect if markdown content has embedded XML components defp has_embedded_components?(content) do String.contains?(content, " [render_earmark_markdown(content) | acc] - [{match_start, match_len}, {tag_start, tag_len}, {attrs_start, attrs_len}] -> + {:self_closing, [{match_start, match_len}, {tag_start, tag_len}, {attrs_start, attrs_len}]} -> before = binary_part(content, 0, match_start) after_index = match_start + match_len rest_content = binary_part(content, after_index, byte_size(content) - after_index) @@ -155,6 +159,46 @@ defmodule PhoenixKit.Blogging.Renderer do |> add_component(tag, attrs) render_mixed_segments(rest_content, acc) + + {:block, indexes} -> + [{match_start, match_len} | _rest] = indexes + before = binary_part(content, 0, match_start) + after_index = match_start + match_len + rest_content = binary_part(content, after_index, byte_size(content) - after_index) + fragment = binary_part(content, match_start, match_len) + + acc = + acc + |> maybe_add_markdown(before) + |> add_block_component(fragment) + + render_mixed_segments(rest_content, acc) + end + end + + defp next_component_match(content) do + self_match = Regex.run(@component_regex, content, return: :index) + block_match = Regex.run(@component_block_regex, content, return: :index) + + case {self_match, block_match} do + {nil, nil} -> + nil + + {nil, block} -> + {:block, block} + + {self, nil} -> + {:self_closing, self} + + {self, block} -> + self_start = self |> hd() |> elem(0) + block_start = block |> hd() |> elem(0) + + if self_start <= block_start do + {:self_closing, self} + else + {:block, block} + end end end @@ -168,6 +212,10 @@ defmodule PhoenixKit.Blogging.Renderer do [render_inline_component(tag, attrs) | acc] end + defp add_block_component(acc, fragment) do + [render_block_component(fragment) | acc] + end + # Render individual inline component defp render_inline_component("Image", attrs) do # Parse attributes @@ -216,6 +264,21 @@ defmodule PhoenixKit.Blogging.Renderer do "" end + defp render_block_component(fragment) do + fragment + |> PageBuilder.render_content() + |> case do + {:ok, html} -> + html + |> Safe.to_iodata() + |> IO.iodata_to_binary() + + {:error, reason} -> + Logger.warning("Error rendering block component: #{inspect(reason)}") + "
Error rendering component
" + end + end + # Parse XML attribute string into a map defp parse_xml_attributes(attrs_string) do # Match key="value" or key='value' patterns diff --git a/lib/phoenix_kit_web/live/modules/blogging/editor.ex b/lib/phoenix_kit_web/live/modules/blogging/editor.ex index 3c4515275..86d96355d 100644 --- a/lib/phoenix_kit_web/live/modules/blogging/editor.ex +++ b/lib/phoenix_kit_web/live/modules/blogging/editor.ex @@ -209,6 +209,42 @@ defmodule PhoenixKitWeb.Live.Modules.Blogging.Editor do {:noreply, assign(socket, :show_media_selector, true)} end + def handle_event("open_image_component_selector", _params, socket) do + {:noreply, + socket + |> assign(:show_media_selector, true) + |> assign(:inserting_image_component, true)} + end + + def handle_event("insert_component", %{"component" => "video"}, socket) do + {:noreply, + push_event(socket, "phx:prompt-and-insert", %{ + component: "video", + prompt: "Enter YouTube URL:", + placeholder: "https://youtu.be/dQw4w9WgXcQ" + })} + end + + def handle_event("insert_component", %{"component" => "cta"}, socket) do + template = """ + Button Text + """ + + {:noreply, push_event(socket, "phx:insert-at-cursor", %{text: template})} + end + + def handle_event("insert_video_component", %{"url" => url}, socket) do + template = """ + + + + """ + + {:noreply, push_event(socket, "phx:insert-at-cursor", %{text: template})} + end + def handle_event("clear_featured_image", _params, socket) do # Clear the featured image from the form (form is a simple map, not a struct) updated_form = Map.put(socket.assigns.form, "featured_image_id", "") @@ -435,25 +471,40 @@ defmodule PhoenixKitWeb.Live.Modules.Blogging.Editor do def handle_info({:media_selected, file_ids}, socket) do # Handle the selected file IDs from the media selector modal file_id = List.first(file_ids) + inserting_image_component = Map.get(socket.assigns, :inserting_image_component, false) socket = - if file_id do + if file_id && inserting_image_component do + # Insert image component at cursor via JavaScript + js_code = "window.insertImageComponent && window.insertImageComponent('#{file_id}')" + socket - |> assign(:form, update_form_with_media(socket.assigns.form, file_id)) - |> assign(:has_pending_changes, true) |> assign(:show_media_selector, false) - |> put_flash(:info, gettext("Featured image selected")) - |> push_event("changes-status", %{has_changes: true}) + |> assign(:inserting_image_component, false) + |> put_flash(:info, gettext("Image component inserted")) + |> push_event("exec-js", %{js: js_code}) else - socket - |> assign(:show_media_selector, false) + if file_id do + socket + |> assign(:form, update_form_with_media(socket.assigns.form, file_id)) + |> assign(:has_pending_changes, true) + |> assign(:show_media_selector, false) + |> put_flash(:info, gettext("Featured image selected")) + |> push_event("changes-status", %{has_changes: true}) + else + socket + |> assign(:show_media_selector, false) + end end {:noreply, socket} end def handle_info({:media_selector_closed}, socket) do - {:noreply, assign(socket, :show_media_selector, false)} + {:noreply, + socket + |> assign(:show_media_selector, false) + |> assign(:inserting_image_component, false)} end defp create_new_post(socket, params) do @@ -701,7 +752,7 @@ defmodule PhoenixKitWeb.Live.Modules.Blogging.Editor do DateTime.utc_now() |> floor_datetime_to_minute() |> DateTime.to_iso8601(), - "featured_image_id" => post.metadata.featured_image_id || "" + "featured_image_id" => Map.get(post.metadata, :featured_image_id, "") } form = diff --git a/lib/phoenix_kit_web/live/modules/blogging/editor.html.heex b/lib/phoenix_kit_web/live/modules/blogging/editor.html.heex index 83e515387..3fa6e7537 100644 --- a/lib/phoenix_kit_web/live/modules/blogging/editor.html.heex +++ b/lib/phoenix_kit_web/live/modules/blogging/editor.html.heex @@ -10,6 +10,143 @@ >