Error rendering markdown
" + cond do + is_pure_phk_content?(content) -> + render_phk_content(content) + + has_embedded_components?(content) -> + render_mixed_content(content) + + true -> + render_earmark_markdown(content) end end) - Logger.debug("Markdown render time: #{time}μs", content_size: byte_size(content)) + Logger.debug("Content render time: #{time}μs", content_size: byte_size(content)) result end def render_markdown(_), do: "" + # Detect if content is pure .phk XML format (starts withError rendering page content
" + end + end + + # Render markdown using Earmark + defp render_earmark_markdown(content) do + content = normalize_markdown(content) + + case Earmark.as_html(content, %Earmark.Options{ + code_class_prefix: "language-", + smartypants: true, + gfm: true, + escape: false + }) do + {:ok, html, _warnings} -> html + {:error, _html, _errors} -> "Error rendering markdown
" + end + end + + defp normalize_markdown(content) when is_binary(content) do + # Remove leading indentation before Markdown headings (e.g., " ## Title") + Regex.replace(~r/^[ \t]+(?=#)/m, content, "") + end + + defp normalize_markdown(content), do: content + + # Render mixed content: markdown with embedded XML components + defp render_mixed_content(content) when content == "" or is_nil(content), do: "" + + defp render_mixed_content(content) do + content + |> render_mixed_segments([]) + |> Enum.reverse() + |> Enum.join() + end + + defp render_mixed_segments("", acc), do: acc + + defp render_mixed_segments(content, acc) do + case Regex.run(@component_regex, content, return: :index) do + nil -> + [render_earmark_markdown(content) | acc] + + [{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) + tag = binary_part(content, tag_start, tag_len) + attrs = binary_part(content, attrs_start, attrs_len) + + acc = + acc + |> maybe_add_markdown(before) + |> add_component(tag, attrs) + + render_mixed_segments(rest_content, acc) + end + end + + defp maybe_add_markdown(acc, ""), do: acc + + defp maybe_add_markdown(acc, text) do + [render_earmark_markdown(text) | acc] + end + + defp add_component(acc, tag, attrs) do + [render_inline_component(tag, attrs) | acc] + end + + # Render individual inline component + defp render_inline_component("Image", attrs) do + # Parse attributes + attr_map = parse_xml_attributes(attrs) + + assigns = %{ + __changed__: nil, + attributes: attr_map, + variant: "default", + content: nil, + children: [] + } + + case PhoenixKitWeb.Components.Blogging.Image.render(assigns) do + rendered when is_struct(rendered) -> + rendered + |> Phoenix.HTML.Safe.to_iodata() + |> IO.iodata_to_binary() + + html when is_binary(html) -> + html + end + rescue + error -> + Logger.warning("Error rendering Image component: #{inspect(error)}") + "-# {@content} -#
-# """ -# end -# end + attr :content, :string, required: true + attr :attributes, :map, default: %{} + attr :variant, :string, default: "default" + + def render(assigns) do + ~H""" ++ {@content} +
+ """ + end +end diff --git a/lib/phoenix_kit_web/live/modules/blogging/context/page_builder.ex b/lib/phoenix_kit_web/live/modules/blogging/context/page_builder.ex index ee9907c57..25ddc0a23 100644 --- a/lib/phoenix_kit_web/live/modules/blogging/context/page_builder.ex +++ b/lib/phoenix_kit_web/live/modules/blogging/context/page_builder.ex @@ -11,132 +11,120 @@ defmodule PhoenixKitWeb.Live.Modules.Blogging.PageBuilder do 6. Render to HTML """ - # ============================================================================ - # COMMENTED OUT: Component-based rendering system - # ============================================================================ - # This module was part of an experimental component-based page building system - # using XML-style markup (.phk files) with swappable design variants. - # See related files: - # - lib/phoenix_kit/blogging/page_builder/parser.ex - # - lib/phoenix_kit/blogging/page_builder/renderer.ex - # - lib/phoenix_kit_web/components/blogging/*.ex - # - priv/blogging/blog/*/en.phk (sample files) - # ============================================================================ - - # alias PhoenixKitWeb.Live.Modules.Blogging.PageBuilder.Parser - # alias PhoenixKitWeb.Live.Modules.Blogging.PageBuilder.Renderer - - # @type assigns :: map() - # @type ast :: map() - # @type render_result :: {:ok, Phoenix.LiveView.Rendered.t()} | {:error, term()} - - # @doc """ - # Renders a .phk page file to HTML. - # - # ## Examples - # - # iex> PageBuilder.render_page("/path/to/page.phk", %{user: %{name: "Alice"}}) - # {:ok, rendered_html} - # """ - # @spec render_page(String.t(), assigns()) :: render_result() - # def render_page(page_path, assigns \\ %{}) do - # with {:ok, content} <- read_page_file(page_path), - # {:ok, ast} <- parse_to_ast(content), - # {:ok, ast_with_data} <- inject_dynamic_data(ast, assigns), - # {:ok, resolved} <- resolve_components(ast_with_data), - # {:ok, themed} <- apply_theme(resolved, assigns), - # {:ok, html} <- render_to_html(themed, assigns) do - # {:ok, html} - # else - # {:error, reason} -> {:error, reason} - # end - # end - - # @doc """ - # Renders .phk content directly (without file path). - # """ - # @spec render_content(String.t(), assigns()) :: render_result() - # def render_content(content, assigns \\ %{}) do - # with {:ok, ast} <- parse_to_ast(content), - # {:ok, ast_with_data} <- inject_dynamic_data(ast, assigns), - # {:ok, resolved} <- resolve_components(ast_with_data), - # {:ok, themed} <- apply_theme(resolved, assigns), - # {:ok, html} <- render_to_html(themed, assigns) do - # {:ok, html} - # else - # {:error, reason} -> {:error, reason} - # end - # end - - # # Step 1: Read .phk file - # defp read_page_file(page_path) do - # case File.read(page_path) do - # {:ok, content} -> {:ok, content} - # {:error, reason} -> {:error, {:file_read_error, reason}} - # end - # end - - # # Step 2: Parse XML to AST - # defp parse_to_ast(content) do - # Parser.parse(content) - # end - - # # Step 3: Inject dynamic data (replace {{variable}} placeholders) - # defp inject_dynamic_data(ast, assigns) do - # {:ok, inject_assigns(ast, assigns)} - # end - - # # Step 4: Resolve components (map XML tags to actual component modules) - # defp resolve_components(ast) do - # {:ok, ast} - # end - - # # Step 5: Apply theme/variant settings - # defp apply_theme(ast, _assigns) do - # {:ok, ast} - # end - - # # Step 6: Render to HTML - # defp render_to_html(ast, assigns) do - # Renderer.render(ast, assigns) - # end - - # # Recursively inject assigns into AST nodes - # defp inject_assigns(ast, assigns) when is_map(ast) do - # ast - # |> Map.update(:content, nil, &inject_assigns(&1, assigns)) - # |> Map.update(:attributes, %{}, &inject_assigns(&1, assigns)) - # |> Map.update(:children, [], &inject_assigns(&1, assigns)) - # end - - # defp inject_assigns(ast, assigns) when is_list(ast) do - # Enum.map(ast, &inject_assigns(&1, assigns)) - # end - - # defp inject_assigns(content, assigns) when is_binary(content) do - # interpolate_string(content, assigns) - # end - - # defp inject_assigns(value, _assigns), do: value - - # # Interpolate {{variable}} placeholders - # defp interpolate_string(string, assigns) do - # Regex.replace(~r/\{\{([^}]+)\}\}/, string, fn _, path -> - # get_nested_value(assigns, String.trim(path)) |> to_string() - # end) - # end - - # # Get nested value from assigns (e.g., "user.name" -> assigns.user.name) - # defp get_nested_value(map, path) do - # path - # |> String.split(".") - # |> Enum.reduce(map, fn key, acc -> - # case acc do - # %{} -> Map.get(acc, key) || Map.get(acc, String.to_existing_atom(key)) - # _ -> nil - # end - # end) - # rescue - # _ -> "" - # end + alias PhoenixKitWeb.Live.Modules.Blogging.PageBuilder.Parser + alias PhoenixKitWeb.Live.Modules.Blogging.PageBuilder.Renderer + + @type assigns :: map() + @type ast :: map() + @type render_result :: {:ok, Phoenix.LiveView.Rendered.t()} | {:error, term()} + + @doc """ + Renders a .phk page file to HTML. + + ## Examples + + iex> PageBuilder.render_page("/path/to/page.phk", %{user: %{name: "Alice"}}) + {:ok, rendered_html} + """ + @spec render_page(String.t(), assigns()) :: render_result() + def render_page(page_path, assigns \\ %{}) do + with {:ok, content} <- read_page_file(page_path), + {:ok, ast} <- parse_to_ast(content), + {:ok, ast_with_data} <- inject_dynamic_data(ast, assigns), + {:ok, resolved} <- resolve_components(ast_with_data), + {:ok, themed} <- apply_theme(resolved, assigns), + {:ok, html} <- render_to_html(themed, assigns) do + {:ok, html} + else + {:error, reason} -> {:error, reason} + end + end + + @doc """ + Renders .phk content directly (without file path). + """ + @spec render_content(String.t(), assigns()) :: render_result() + def render_content(content, assigns \\ %{}) do + with {:ok, ast} <- parse_to_ast(content), + {:ok, ast_with_data} <- inject_dynamic_data(ast, assigns), + {:ok, resolved} <- resolve_components(ast_with_data), + {:ok, themed} <- apply_theme(resolved, assigns), + {:ok, html} <- render_to_html(themed, assigns) do + {:ok, html} + else + {:error, reason} -> {:error, reason} + end + end + + # Step 1: Read .phk file + defp read_page_file(page_path) do + case File.read(page_path) do + {:ok, content} -> {:ok, content} + {:error, reason} -> {:error, {:file_read_error, reason}} + end + end + + # Step 2: Parse XML to AST + defp parse_to_ast(content) do + Parser.parse(content) + end + + # Step 3: Inject dynamic data (replace {{variable}} placeholders) + defp inject_dynamic_data(ast, assigns) do + {:ok, inject_assigns(ast, assigns)} + end + + # Step 4: Resolve components (map XML tags to actual component modules) + defp resolve_components(ast) do + {:ok, ast} + end + + # Step 5: Apply theme/variant settings + defp apply_theme(ast, _assigns) do + {:ok, ast} + end + + # Step 6: Render to HTML + defp render_to_html(ast, assigns) do + Renderer.render(ast, assigns) + end + + # Recursively inject assigns into AST nodes + defp inject_assigns(ast, assigns) when is_map(ast) do + ast + |> Map.update(:content, nil, &inject_assigns(&1, assigns)) + |> Map.update(:attributes, %{}, &inject_assigns(&1, assigns)) + |> Map.update(:children, [], &inject_assigns(&1, assigns)) + end + + defp inject_assigns(ast, assigns) when is_list(ast) do + Enum.map(ast, &inject_assigns(&1, assigns)) + end + + defp inject_assigns(content, assigns) when is_binary(content) do + interpolate_string(content, assigns) + end + + defp inject_assigns(value, _assigns), do: value + + # Interpolate {{variable}} placeholders + defp interpolate_string(string, assigns) do + Regex.replace(~r/\{\{([^}]+)\}\}/, string, fn _, path -> + get_nested_value(assigns, String.trim(path)) |> to_string() + end) + end + + # Get nested value from assigns (e.g., "user.name" -> assigns.user.name) + defp get_nested_value(map, path) do + path + |> String.split(".") + |> Enum.reduce(map, fn key, acc -> + case acc do + %{} -> Map.get(acc, key) || Map.get(acc, String.to_existing_atom(key)) + _ -> nil + end + end) + rescue + _ -> "" + end end diff --git a/lib/phoenix_kit_web/live/modules/blogging/context/page_builder/parser.ex b/lib/phoenix_kit_web/live/modules/blogging/context/page_builder/parser.ex index 6d59a9e85..2c8894217 100644 --- a/lib/phoenix_kit_web/live/modules/blogging/context/page_builder/parser.ex +++ b/lib/phoenix_kit_web/live/modules/blogging/context/page_builder/parser.ex @@ -1,156 +1,152 @@ -# ============================================================================ -# COMMENTED OUT: Component-based rendering system - XML Parser -# ============================================================================ -# This module was part of an experimental component-based page building system -# using XML-style markup (.phk files) with swappable design variants. -# Related to: lib/phoenix_kit/blogging/page_builder.ex -# ============================================================================ - -# defmodule PhoenixKitWeb.Live.Modules.Blogging.PageBuilder.Parser do -# @moduledoc """ -# Parses .phk (PhoenixKit) XML-style markup into an AST. -# -# Example input: -# ```xml -#