defmodule ElixirAiWeb.JsonDisplay do use Phoenix.Component attr :json, :any, required: true attr :class, :string, default: nil attr :inline, :boolean, default: false def json_display(%{json: json, inline: inline} = assigns) do formatted = case json do nil -> "" "" -> "" s when is_binary(s) -> case Jason.decode(s) do {:ok, decoded} -> Jason.encode!(decoded, pretty: !inline) _ -> s end other -> Jason.encode!(other, pretty: !inline) end assigns = assign(assigns, :_highlighted, json_to_html(formatted)) ~H"""
<%= @_highlighted %>
{@_highlighted} """ end @token_colors %{ key: "text-sky-300", string: "text-emerald-400/80", keyword: "text-violet-400", number: "text-orange-300/80", colon: "text-seafoam-500/50", punctuation: "text-seafoam-500/50", quote: "text-seafoam-500/50" } # Converts a plain JSON string into a Phoenix.HTML.safe value with # tokens coloured by token type. defp json_to_html(""), do: Phoenix.HTML.raw("") defp json_to_html(str) do # Capture groups (in order): # 1 string literal "..." # 2 keyword true | false | null # 3 number -?digits with optional frac/exp # 4 punctuation { } [ ] , : # 5 whitespace spaces / newlines / tabs # 6 fallback any other single char token_re = ~r/(".(?:[^"\\]|\\.)*")|(true|false|null)|(-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?)|([{}\[\],:])|(\s+)|(.)/s tokens = Regex.scan(token_re, str, capture: :all_but_first) {parts, _, _} = Enum.reduce(tokens, {[], :val, []}, fn groups, {acc, state, ctx} -> [string_tok, keyword_tok, number_tok, punct_tok, whitespace_tok, fallback_tok] = pad_groups(groups, 6) cond do string_tok != "" -> {color, next_state} = if state == :key, do: {@token_colors.key, :after_key}, else: {@token_colors.string, :after_val} content = string_tok |> String.slice(1..-2//1) |> html_escape() quote_span = color_span(@token_colors.quote, """) {[quote_span <> color_span(color, content) <> quote_span | acc], next_state, ctx} keyword_tok != "" -> {[color_span(@token_colors.keyword, keyword_tok) | acc], :after_val, ctx} number_tok != "" -> {[color_span(@token_colors.number, number_tok) | acc], :after_val, ctx} punct_tok != "" -> {next_state, next_ctx} = advance_state(punct_tok, state, ctx) color = if punct_tok == ":", do: @token_colors.colon, else: @token_colors.punctuation {[color_span(color, punct_tok) | acc], next_state, next_ctx} whitespace_tok != "" -> {[whitespace_tok | acc], state, ctx} fallback_tok != "" -> {[html_escape(fallback_tok) | acc], state, ctx} true -> {acc, state, ctx} end end) Phoenix.HTML.raw(parts |> Enum.reverse() |> Enum.join()) end # State transitions driven by punctuation tokens. # State :key → we are about to read an object key. # State :val → we are about to read a value. # State :after_key / :after_val → consumed the token; awaiting : or ,. defp advance_state("{", _, ctx), do: {:key, [:obj | ctx]} defp advance_state("[", _, ctx), do: {:val, [:arr | ctx]} defp advance_state("}", _, [_ | ctx]), do: {:after_val, ctx} defp advance_state("}", _, []), do: {:after_val, []} defp advance_state("]", _, [_ | ctx]), do: {:after_val, ctx} defp advance_state("]", _, []), do: {:after_val, []} defp advance_state(":", _, ctx), do: {:val, ctx} defp advance_state(",", _, [:obj | _] = ctx), do: {:key, ctx} defp advance_state(",", _, ctx), do: {:val, ctx} defp advance_state(_, state, ctx), do: {state, ctx} defp pad_groups(list, n), do: list ++ List.duplicate("", max(0, n - length(list))) defp color_span(class, content), do: ~s|#{content}| defp html_escape(str) do str |> String.replace("&", "&") |> String.replace("<", "<") |> String.replace(">", ">") end end