Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 67 additions & 4 deletions lib/phoenix_kit/blogging/renderer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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, "<Image ") ||
String.contains?(content, "<Hero ") ||
String.contains?(content, "<CTA ")
String.contains?(content, "<Hero") ||
String.contains?(content, "<CTA") ||
String.contains?(content, "<Headline") ||
String.contains?(content, "<Subheadline") ||
String.contains?(content, "<Video")
end

# Render .phk content using PageBuilder
Expand Down Expand Up @@ -138,11 +142,11 @@ defmodule PhoenixKit.Blogging.Renderer do
defp render_mixed_segments("", acc), do: acc

defp render_mixed_segments(content, acc) do
case Regex.run(@component_regex, content, return: :index) do
case next_component_match(content) do
nil ->
[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)
Expand All @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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)}")
"<div class='error'>Error rendering component</div>"
end
end

# Parse XML attribute string into a map
defp parse_xml_attributes(attrs_string) do
# Match key="value" or key='value' patterns
Expand Down
69 changes: 60 additions & 9 deletions lib/phoenix_kit_web/live/modules/blogging/editor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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 = """
<CTA primary="true" action="/your-link">Button Text</CTA>
"""

{:noreply, push_event(socket, "phx:insert-at-cursor", %{text: template})}
end

def handle_event("insert_video_component", %{"url" => url}, socket) do
template = """

<Video url="#{url}">
Optional caption text
</Video>

"""

{: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", "")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 =
Expand Down
Loading
Loading