diff --git a/.circleci/config.yml b/.circleci/config.yml
index 01331099386..b80d39bf912 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -188,11 +188,8 @@ jobs:
command: |
# GHSA-g2wm-735q-3f56: cowlib cookie encoder CRLF injection (low);
# no patched release available yet.
- # GHSA-55hg-8qxv-qj4p / GHSA-833p-95jq-929q / GHSA-mrhx-6pw9-q5fh:
- # phoenix_storybook advisories. Dev-only dep, not compiled into
- # prod and storybook routes are dev-only. Tracked for removal.
sudo -u lightning mix deps.audit \
- --ignore-advisory-ids GHSA-g2wm-735q-3f56,GHSA-55hg-8qxv-qj4p,GHSA-833p-95jq-929q,GHSA-mrhx-6pw9-q5fh \
+ --ignore-advisory-ids GHSA-g2wm-735q-3f56 \
|| echo "deps.audit" >> /tmp/lint_failed
- run:
name: "Check for retired Hex packages"
diff --git a/.formatter.exs b/.formatter.exs
index ddcc82cf79b..ac615d55520 100644
--- a/.formatter.exs
+++ b/.formatter.exs
@@ -4,8 +4,7 @@
inputs: [
"*.{heex,ex,exs}",
"priv/*/seeds.exs",
- "{config,lib,test}/**/*.{heex,ex,exs}",
- "storybook/**/*.exs"
+ "{config,lib,test}/**/*.{heex,ex,exs}"
],
subdirectories: ["priv/*/migrations"],
line_length: 81
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d860a2b8039..df9049127a3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -25,6 +25,11 @@ and this project adheres to
### Changed
+- Migrated off the retired `earmark` markdown dependency in favour of `mdex`.
+ [#4878](https://github.com/OpenFn/lightning/issues/4878)
+- Removed the unused dev-only `phoenix_storybook` dependency, clearing its
+ advisories from the `mix deps.audit` ignore list.
+ [#4846](https://github.com/OpenFn/lightning/issues/4846)
- Bump worker to 1.27.0
### Fixed
diff --git a/RUNNINGLOCAL.md b/RUNNINGLOCAL.md
index e6e243a5276..5dea9d41a76 100644
--- a/RUNNINGLOCAL.md
+++ b/RUNNINGLOCAL.md
@@ -183,8 +183,8 @@ LOCAL_ADAPTORS=true mix phx.server
```
Each path in `OPENFN_ADAPTORS_REPO` must contain a `packages` subdirectory.
-Paths that are missing or unreadable are logged and skipped, so the rest of
-the list still loads.
+Paths that are missing or unreadable are logged and skipped, so the rest of the
+list still loads.
### Problems with Apple Silicon
diff --git a/assets/css/storybook.css b/assets/css/storybook.css
deleted file mode 100644
index 85ef535ea9a..00000000000
--- a/assets/css/storybook.css
+++ /dev/null
@@ -1,11 +0,0 @@
-/* This is your custom storybook stylesheet. */
-@import 'tailwindcss';
-
-/*
- * Put your component styling within the Tailwind utilities layer.
- * See the https://hexdocs.pm/phoenix_storybook/sandboxing.html guide for more info.
- */
-
-.lsb-sandbox {
- font-family: inherit !important;
-}
diff --git a/assets/js/storybook.js b/assets/js/storybook.js
deleted file mode 100644
index 863cce8900d..00000000000
--- a/assets/js/storybook.js
+++ /dev/null
@@ -1,11 +0,0 @@
-// If your components require any hooks or custom uploaders, or if your pages
-// require connect parameters, uncomment the following lines and declare them as
-// such:
-//
-// import * as Hooks from "./hooks";
-// import * as Params from "./params";
-// import * as Uploaders from "./uploaders";
-
-// (function () {
-// window.storybook = { Hooks, Params, Uploaders };
-// })();
diff --git a/codecov.yml b/codecov.yml
index 751bf383d95..2d89f312f83 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -17,7 +17,6 @@ ignore:
- "lib/lightning/release.ex"
- "lib/lightning/sentry_event_filter.ex"
- "test/support/**"
- - "storybook/**"
# Phoenix declarative scaffolding — boot-only or pure DSL
- "lib/lightning_web.ex"
diff --git a/config/config.exs b/config/config.exs
index 729fb033e3c..ccecad5e147 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -102,7 +102,6 @@ config :esbuild,
--external:/fonts/*
--external:/images/*
js/app.js
- js/storybook.js
js/editor/Editor.tsx
js/react/components/DataclipViewer.tsx
js/react/components/CollectionPreviewViewer.tsx
@@ -145,13 +144,6 @@ config :tailwind,
--output=priv/static/assets/app.css
),
cd: Path.expand("..", __DIR__)
- ],
- storybook: [
- args: ~w(
- --input=assets/css/storybook.css
- --output=priv/static/assets/storybook.css
- ),
- cd: Path.expand("..", __DIR__)
]
# Configures Elixir's Logger
diff --git a/config/dev.exs b/config/dev.exs
index ad4124a0307..de1762e2fdc 100644
--- a/config/dev.exs
+++ b/config/dev.exs
@@ -33,8 +33,7 @@ config :lightning, LightningWeb.Endpoint,
# Start the esbuild watcher by calling Esbuild.install_and_run(:default, args)
esbuild:
{Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]},
- tailwind: {Tailwind, :install_and_run, [:default, ~w(--watch)]},
- storybook_tailwind: {Tailwind, :install_and_run, [:storybook, ~w(--watch)]}
+ tailwind: {Tailwind, :install_and_run, [:default, ~w(--watch)]}
]
config :lightning,
@@ -112,8 +111,7 @@ config :lightning, LightningWeb.Endpoint,
~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
~r"priv/gettext/.*(po)$",
~r"lib/lightning_web/(live|components|views)/.*(ex|heex)$",
- ~r"lib/lightning_web/templates/.*(eex)$",
- ~r"storybook/.*(exs)$"
+ ~r"lib/lightning_web/templates/.*(eex)$"
]
]
diff --git a/coveralls.json b/coveralls.json
index 69a568d6024..c51ec3a780f 100644
--- a/coveralls.json
+++ b/coveralls.json
@@ -4,7 +4,6 @@
"lib/lightning/release.ex",
"lib/lightning/sentry_event_filter.ex",
"lib/mix/tasks/install_runtime.ex",
- "test/support/*",
- "storybook/*"
+ "test/support/*"
]
}
\ No newline at end of file
diff --git a/lib/lightning_web/live/ai_assistant/component.ex b/lib/lightning_web/live/ai_assistant/component.ex
index 0c10781b862..4f86cdf769c 100644
--- a/lib/lightning_web/live/ai_assistant/component.ex
+++ b/lib/lightning_web/live/ai_assistant/component.ex
@@ -18,8 +18,6 @@ defmodule LightningWeb.AiAssistant.Component do
require Logger
- @dialyzer {:nowarn_function, process_ast: 2}
-
@default_page_size 20
@message_preview_length 50
@typing_animation_delay_ms 100
@@ -1558,78 +1556,82 @@ defmodule LightningWeb.AiAssistant.Component do
Calendar.strftime(datetime, "%I:%M %p")
end
+ # Default per-element HTML attributes injected into rendered markdown so
+ # assistant messages pick up Tailwind styling. Keyed by HTML tag name.
+ @assistant_messages_attributes %{
+ "a" => %{
+ class: "text-primary-400 hover:text-primary-600",
+ target: "_blank"
+ },
+ "h1" => %{class: "text-2xl font-bold mb-6"},
+ "h2" => %{class: "text-xl font-semibold mb-4 mt-8"},
+ "ol" => %{class: "list-decimal pl-8 space-y-1"},
+ "ul" => %{class: "list-disc pl-8 space-y-1"},
+ "li" => %{class: "text-gray-800"},
+ "p" => %{class: "mt-1 mb-2 text-gray-800"},
+ "pre" => %{
+ class:
+ "rounded-md font-mono bg-slate-100 border-2 border-slate-200 text-slate-800 my-4 p-2 overflow-auto"
+ }
+ }
+
+ # Match Earmark's default of passing raw HTML through untouched. The content
+ # is AI-generated markdown rendered for the same user who prompted it.
+ #
+ # Earmark defaulted to `gfm: true`, so enable the GFM extensions (tables,
+ # strikethrough, bare-URL autolinks, task lists) to keep assistant replies —
+ # which frequently contain tables — rendering as they did before.
+ @mdex_options [
+ extension: [
+ table: true,
+ strikethrough: true,
+ autolink: true,
+ tasklist: true
+ ],
+ render: [unsafe: true]
+ ]
+
attr :id, :string, required: true
attr :content, :string, required: true
attr :attributes, :map, default: %{}
def formatted_content(assigns) do
- assistant_messages_attributes = %{
- "a" => %{
- class: "text-primary-400 hover:text-primary-600",
- target: "_blank"
- },
- "h1" => %{class: "text-2xl font-bold mb-6"},
- "h2" => %{class: "text-xl font-semibold mb-4 mt-8"},
- "ol" => %{class: "list-decimal pl-8 space-y-1"},
- "ul" => %{class: "list-disc pl-8 space-y-1"},
- "li" => %{class: "text-gray-800"},
- "p" => %{class: "mt-1 mb-2 text-gray-800"},
- "pre" => %{
- class:
- "rounded-md font-mono bg-slate-100 border-2 border-slate-200 text-slate-800 my-4 p-2 overflow-auto"
- }
- }
-
merged_attributes =
- Map.merge(assistant_messages_attributes, assigns.attributes)
+ Map.merge(@assistant_messages_attributes, assigns.attributes)
- assigns =
- case Earmark.Parser.as_ast(assigns.content) do
- {:ok, ast, _} ->
- process_ast(ast, merged_attributes) |> raw()
+ rendered =
+ case MDEx.to_html(assigns.content, @mdex_options) do
+ {:ok, html} ->
+ html |> inject_attributes(merged_attributes) |> raw()
- _ ->
+ {:error, _reason} ->
assigns.content
end
- |> then(&assign(assigns, :content, &1))
+
+ assigns = assign(assigns, :content, rendered)
~H"""
{@content}
"""
end
- defp process_ast(ast, attributes) do
- ast
- |> Earmark.Transform.map_ast(&process_node(&1, attributes))
- |> Earmark.Transform.transform()
- end
-
- defp process_node({element_type, attrs, _content, _meta} = node, attributes) do
- case Map.get(attributes, element_type) do
- nil -> node
- attribute_map -> apply_attributes(node, element_type, attrs, attribute_map)
- end
- end
-
- defp process_node(other, _attributes), do: other
-
- defp apply_attributes(node, "code", attrs, attribute_map) do
- case find_class_attr(attrs) do
- {_, [lang]} ->
- trimmed_lang = String.trim(lang)
- Earmark.AstTools.merge_atts_in_node(node, class: trimmed_lang)
-
- _ ->
- Earmark.AstTools.merge_atts_in_node(node, attribute_map)
- end
+ # MDEx node structs cannot carry arbitrary HTML attributes, so inject the
+ # per-element classes into the rendered HTML instead.
+ defp inject_attributes(html, attributes) do
+ Enum.reduce(attributes, html, fn {tag, attrs}, acc ->
+ inject_tag_attributes(acc, tag, attrs)
+ end)
end
- defp apply_attributes(node, _element_type, _attrs, attribute_map) do
- Earmark.AstTools.merge_atts_in_node(node, attribute_map)
- end
+ defp inject_tag_attributes(html, tag, attrs) do
+ attrs_string =
+ Enum.map_join(attrs, " ", fn {key, value} -> ~s(#{key}="#{value}") end)
- defp find_class_attr(attrs) do
- Enum.find(attrs, fn {attr, _} -> attr == "class" end)
+ String.replace(
+ html,
+ ~r/<#{Regex.escape(tag)}(?=[\s>])/,
+ ~s(<#{tag} #{attrs_string})
+ )
end
attr :user, Lightning.Accounts.User, default: nil
diff --git a/lib/lightning_web/router.ex b/lib/lightning_web/router.ex
index e8f64c1fe53..501d737d3bd 100644
--- a/lib/lightning_web/router.ex
+++ b/lib/lightning_web/router.ex
@@ -306,18 +306,6 @@ defmodule LightningWeb.Router do
end
do_in(:dev) do
- import PhoenixStorybook.Router
-
- scope "/" do
- storybook_assets()
- end
-
- scope "/" do
- pipe_through :browser
-
- live_storybook("/storybook", backend_module: LightningWeb.Storybook)
- end
-
scope "/dev" do
pipe_through :browser
diff --git a/lib/lightning_web/storybook.ex b/lib/lightning_web/storybook.ex
deleted file mode 100644
index f7b8a91dbf9..00000000000
--- a/lib/lightning_web/storybook.ex
+++ /dev/null
@@ -1,15 +0,0 @@
-defmodule LightningWeb.Storybook do
- @moduledoc false
-
- use Lightning.BuildMacros
-
- do_in(:dev) do
- use PhoenixStorybook,
- otp_app: :lightning_web,
- content_path: Path.expand("../../storybook", __DIR__),
- # assets path are remote path, not local file-system paths
- css_path: "/assets/storybook.css",
- js_path: "/assets/storybook.js",
- sandbox_class: "lightning-web"
- end
-end
diff --git a/mix.exs b/mix.exs
index dfa0fe27977..d7061c3d622 100644
--- a/mix.exs
+++ b/mix.exs
@@ -120,7 +120,6 @@ defmodule Lightning.MixProject do
{:phoenix_live_dashboard, "~> 0.8"},
{:phoenix_live_reload, "~> 1.5", only: :dev},
{:phoenix_live_view, "~> 1.0.17"},
- {:phoenix_storybook, "~> 0.9.2", only: :dev},
{:cors_plug, "~> 3.0"},
{:plug_cowboy, "~> 2.5"},
{:postgrex, ">= 0.0.0"},
@@ -152,7 +151,7 @@ defmodule Lightning.MixProject do
{:eqrcode, "~> 0.2"},
# Github API Secret Encoding
{:enacl, github: "aeternity/enacl", branch: "master"},
- {:earmark, "~> 1.4"},
+ {:mdex, "~> 0.13"},
{:eventually, "~> 1.1", only: [:test]},
{:benchee, "~> 1.5.0", only: :dev},
{:statistics, "~> 0.6", only: :dev},
diff --git a/mix.lock b/mix.lock
index 994983eadf9..dee1d25a0e0 100644
--- a/mix.lock
+++ b/mix.lock
@@ -29,7 +29,6 @@
"deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"},
"dialyxir": {:hex, :dialyxir, "1.4.6", "7cca478334bf8307e968664343cbdb432ee95b4b68a9cba95bdabb0ad5bdfd9a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "8cf5615c5cd4c2da6c501faae642839c8405b49f8aa057ad4ae401cb808ef64d"},
"dotenvy": {:hex, :dotenvy, "1.1.0", "316aee89c11a4ec8be3d74a69d17d17ea2e21e633e0cac9f155cf420e237ccb4", [:mix], [], "hexpm", "0519bda67fdfa1c22279c2654b2f292485f0caae7360fe29205f74f28a93df18"},
- "earmark": {:hex, :earmark, "1.4.48", "5f41e579d85ef812351211842b6e005f6e0cef111216dea7d4b9d58af4608434", [:mix], [], "hexpm", "a461a0ddfdc5432381c876af1c86c411fd78a25790c75023c7a4c035fdc858f9"},
"earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"},
"ecto": {:hex, :ecto, "3.13.2", "7d0c0863f3fc8d71d17fc3ad3b9424beae13f02712ad84191a826c7169484f01", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "669d9291370513ff56e7b7e7081b7af3283d02e046cf3d403053c557894a0b3e"},
"ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"},
@@ -78,10 +77,10 @@
"libcluster_postgres": {:hex, :libcluster_postgres, "0.2.0", "14a5064b78f891c46935a66489454814d949a52b447fc1daff5d4a440e6f5847", [:mix], [{:libcluster, "~> 3.3", [hex: :libcluster, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "ab2e952371c5a0a0fcb263216c7eae2a2267977b3bb3236650daed3054a93edd"},
"live_debugger": {:hex, :live_debugger, "0.3.2", "b67baa8ed6a4329fe0c6aaf21a403cce4d0bac9b33d90707fe2609108614ac69", [:mix], [{:igniter, ">= 0.5.40 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 0.20.4 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "5050b37af05a2b84d429e7256a41d3612283c4c802edd23e6eeb4e0b6fc2a712"},
"makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"},
- "makeup_eex": {:hex, :makeup_eex, "2.0.2", "88983b72aadb2e8408b06f7c9413804ce7eae2ca2a5a35cb738c6a9cb393c155", [:mix], [{:makeup, "~> 1.2.1 or ~> 1.3", [hex: :makeup, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_html, "~> 0.2.0 or ~> 1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 1.2", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "30ac121dda580298ff3378324ffaec94aad5a5b67e0cc6af177c67d5f45629b9"},
"makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"},
"makeup_erlang": {:hex, :makeup_erlang, "1.0.3", "4252d5d4098da7415c390e847c814bad3764c94a814a0b4245176215615e1035", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "953297c02582a33411ac6208f2c6e55f0e870df7f80da724ed613f10e6706afd"},
- "makeup_html": {:hex, :makeup_html, "0.2.0", "9f810da8d43d625ccd3f7ea25997e588fa541d80e0a8c6b895157ad5c7e9ca13", [:mix], [{:makeup, "~> 1.2", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "0856f7beb9a6a642ab1307e06d990fe39f0ba58690d0b8e662aa2e027ba331b2"},
+ "mdex": {:hex, :mdex, "0.13.1", "2af3da1e0ec1594d9fc4c0134031a0b48397092963eef87448a7a1c2b9332663", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:lumis, "~> 0.1", [hex: :lumis, repo: "hexpm", optional: true]}, {:mdex_native, ">= 0.2.2", [hex: :mdex_native, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}], "hexpm", "99e934e977ea4045914870616baae8d4e1513aa0309f769ed65a681ebe5e0f02"},
+ "mdex_native": {:hex, :mdex_native, "0.2.2", "adc230643a173b4a2b3593c450f56c41e51714c51f59bb8e528993810373cb92", [:mix], [{:rustler, "~> 0.32", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.7", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "f16b0548dca63b91c134316cc89c1160119e8f308db57bcc5fc058206a7df32d"},
"meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"},
@@ -113,7 +112,6 @@
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.6.2", "b18b0773a1ba77f28c52decbb0f10fd1ac4d3ae5b8632399bbf6986e3b665f62", [:mix], [{:file_system, "~> 0.2.10 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "d1f89c18114c50d394721365ffb428cce24f1c13de0467ffa773e2ff4a30d5b9"},
"phoenix_live_view": {:hex, :phoenix_live_view, "1.0.18", "943431edd0ef8295ffe4949f0897e2cb25c47d3d7ebba2b008d7c68598b887f1", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "724934fd0a68ecc57281cee863674454b06163fed7f5b8005b5e201ba4b23316"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"},
- "phoenix_storybook": {:hex, :phoenix_storybook, "0.9.2", "6bc80f89284e47c8f53a39ddf80ce19dff459d3a3490dce11349a280c6192762", [:mix], [{:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:jason, "~> 1.3", [hex: :jason, repo: "hexpm", optional: true]}, {:makeup_eex, "~> 2.0.2", [hex: :makeup_eex, repo: "hexpm", optional: false]}, {:makeup_html, "~> 0.2.0", [hex: :makeup_html, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html_helpers, "~> 1.0", [hex: :phoenix_html_helpers, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}], "hexpm", "4dbfd43e85a5d578235fb53df38f9f7ba0809156b56d55a52e7cec0b2648aba2"},
"phoenix_swoosh": {:hex, :phoenix_swoosh, "1.2.1", "b74ccaa8046fbc388a62134360ee7d9742d5a8ae74063f34eb050279de7a99e1", [:mix], [{:finch, "~> 0.8", [hex: :finch, repo: "hexpm", optional: true]}, {:hackney, "~> 1.10", [hex: :hackney, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.5", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "4000eeba3f9d7d1a6bf56d2bd56733d5cadf41a7f0d8ffe5bb67e7d667e204a2"},
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
"phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"},
@@ -130,7 +128,6 @@
"replug": {:hex, :replug, "0.1.0", "61d35f8c873c0078a23c49579a48f36e45789414b1ec0daee3fd5f4e34221f23", [:mix], [{:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "f71f7a57e944e854fe4946060c6964098e53958074c69fb844b96e0bd58cfa60"},
"req": {:hex, :req, "0.5.1", "90584216d064389a4ff2d4279fe2c11ff6c812ab00fa01a9fb9d15457f65ba70", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "7ea96a1a95388eb0fefa92d89466cdfedba24032794e5c1147d78ec90db7edca"},
"retry": {:hex, :retry, "0.19.0", "aeb326d87f62295d950f41e1255fe6f43280a1b390d36e280b7c9b00601ccbc2", [:mix], [], "hexpm", "85ef376aa60007e7bff565c366310966ec1bd38078765a0e7f20ec8a220d02ca"},
- "rustler": {:hex, :rustler, "0.32.1", "f4cf5a39f9e85d182c0a3f75fa15b5d0add6542ab0bf9ceac6b4023109ebd3fc", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.6", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "b96be75526784f86f6587f051bc8d6f4eaff23d6e0f88dbcfe4d5871f52946f7"},
"rustler_precompiled": {:hex, :rustler_precompiled, "0.8.3", "4e741024b0b097fe783add06e53ae9a6f23ddc78df1010f215df0c02915ef5a8", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "c23f5f33cb6608542de4d04faf0f0291458c352a4648e4d28d17ee1098cddcc4"},
"scrivener": {:hex, :scrivener, "2.7.2", "1d913c965ec352650a7f864ad7fd8d80462f76a32f33d57d1e48bc5e9d40aba2", [:mix], [], "hexpm", "7866a0ec4d40274efbee1db8bead13a995ea4926ecd8203345af8f90d2b620d9"},
"sentry": {:hex, :sentry, "10.9.0", "503575bc98ef268ad75e9792e17637ab7b270ed8036614f777a1833272409016", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_ownership, "~> 0.3.0 or ~> 1.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 0.20 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.6", [hex: :plug, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "9abf07e6a757f6650e2429b5773f546ff119f6980b9bb02067a7eb510a75c9f2"},
@@ -156,9 +153,7 @@
"unsafe": {:hex, :unsafe, "1.0.2", "23c6be12f6c1605364801f4b47007c0c159497d0446ad378b5cf05f1855c0581", [:mix], [], "hexpm", "b485231683c3ab01a9cd44cb4a79f152c6f3bb87358439c6f68791b85c2df675"},
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
"websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"},
- "weir": {:git, "https://github.com/OpenFn/weir.git", "41e20bfd70845f29b15fed52f441de4719b48b27", []},
"y_ex": {:hex, :y_ex, "0.8.0", "e1591d97a487a15fe93eb29b88685d0ccb6f76403cdd2b8c60e9cebb9a2d204e", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, ">= 0.6.0", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "d2ce875481c28896d5d9037d8cb5d859ddbcfb047dcfebdcd0d33c6ebfd3d506"},
"yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"},
"yaml_elixir": {:hex, :yaml_elixir, "2.12.1", "d74f2d82294651b58dac849c45a82aaea639766797359baff834b64439f6b3f4", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "d9ac16563c737d55f9bfeed7627489156b91268a3a21cd55c54eb2e335207fed"},
- "yex": {:hex, :yex, "0.0.1", "99ad1448ac9f7482b40fea8fc5ba23c92933a435b96935b079854e362e8b2353", [:mix], [{:rustler, "~> 0.32.1", [hex: :rustler, repo: "hexpm", optional: false]}], "hexpm", "8304c754ea0856f88f5f1f089191641393fae2791780a8b8865f7b4f9c6069b6"},
}
diff --git a/storybook/_root.index.exs b/storybook/_root.index.exs
deleted file mode 100644
index edef47c744e..00000000000
--- a/storybook/_root.index.exs
+++ /dev/null
@@ -1,19 +0,0 @@
-defmodule Storybook.Root do
- @moduledoc false
- # See https://hexdocs.pm/phoenix_storybook/PhoenixStorybook.Index.html for full index
- # documentation.
-
- use PhoenixStorybook.Index
-
- def folder_name, do: "Storybook"
- def folder_icon, do: {:fa, "book-open", :light, "lsb-mr-1"}
- def folder_open?, do: true
- def folder_index, do: 0
-
- def entry("welcome") do
- [
- name: "Welcome Page",
- icon: {:fa, "hand-wave", :thin}
- ]
- end
-end
diff --git a/storybook/common/button.story.exs b/storybook/common/button.story.exs
deleted file mode 100644
index e937f6848e0..00000000000
--- a/storybook/common/button.story.exs
+++ /dev/null
@@ -1,31 +0,0 @@
-defmodule LightningWeb.Storybook.Common.Button do
- alias LightningWeb.Components.Common
- use PhoenixStorybook.Story, :component
-
- # required
- def function, do: &Common.button/1
-
- def variations do
- [
- %Variation{
- id: :default,
- description: "Default button",
- attributes: %{text: "I'm a button"},
- slots: []
- },
- %Variation{
- id: :with_icon,
- description: "With an Icon",
- attributes: %{},
- slots: [
- """
-
-
- Remove
-
- """
- ]
- }
- ]
- end
-end
diff --git a/storybook/form/text_field.story.exs b/storybook/form/text_field.story.exs
deleted file mode 100644
index 2c4b292b285..00000000000
--- a/storybook/form/text_field.story.exs
+++ /dev/null
@@ -1,26 +0,0 @@
-defmodule LightningWeb.Storybook.Form.TextField do
- use PhoenixStorybook.Story, :component
- alias LightningWeb.Components.Form
-
- # required
- def function, do: &Form.text_field/1
-
- def template do
- """
- <.form for={%{}} as={:story} :let={f} class="w-full">
- <.lsb-variation form={f}/>
-
- """
- end
-
- def variations do
- [
- %Variation{
- id: :default_text_input,
- attributes: %{
- field: :name
- }
- }
- ]
- end
-end
diff --git a/storybook/welcome.story.exs b/storybook/welcome.story.exs
deleted file mode 100644
index b3fafbf7fb5..00000000000
--- a/storybook/welcome.story.exs
+++ /dev/null
@@ -1,102 +0,0 @@
-defmodule Storybook.MyPage do
- # See https://hexdocs.pm/phoenix_storybook/PhoenixStorybook.Story.html for full story
- # documentation.
- use PhoenixStorybook.Story, :page
-
- def doc, do: "Your very first steps into using Phoenix Storybook"
-
- # Declare an optional tab-based navigation in your page:
- def navigation do
- [
- {:welcome, "Welcome", {:fa, "hand-wave", :thin}},
- {:components, "Components", {:fa, "toolbox", :thin}},
- {:sandboxing, "Sandboxing", {:fa, "box-check", :thin}},
- {:icons, "Icons", {:fa, "icons", :thin}}
- ]
- end
-
- # This is a dummy fonction that you should replace with your own HEEx content.
- def render(assigns = %{tab: :welcome}) do
- ~H"""
-
-
- We generated your storybook with an example of a page and a component.
- Explore the generated *.story.exs
- files in your /storybook
- directory. When you're ready to add your own, just drop your new story & index files into the same directory and refresh your storybook.
-
-
-
- Here are a few docs you might be interested in:
-
-
- <.description_list items={[
- {"Create a new Story", doc_link("Story")},
- {"Display components using Variations", doc_link("Stories.Variation")},
- {"Group components using VariationGroups",
- doc_link("Stories.VariationGroup")},
- {"Organize the sidebar with Index files", doc_link("Index")}
- ]} />
-
-
- This should be enough to get you started, but you can use the tabs in the upper-right corner of this page to check out advanced usage guides.
-
-
- """
- end
-
- def render(assigns = %{tab: guide})
- when guide in ~w(components sandboxing icons)a do
- assigns =
- assign(assigns,
- guide: guide,
- guide_content: PhoenixStorybook.Guides.markup("#{guide}.md")
- )
-
- ~H"""
-
-
- This and other guides are also available on HexDocs.
-
-
-
- {Phoenix.HTML.raw(@guide_content)}
-
- """
- end
-
- defp description_list(assigns) do
- ~H"""
-
-
-
- <%= for {dt, link} <- @items do %>
-
- <% end %>
-
-
-
- """
- end
-
- defp doc_link(page) do
- "https://hexdocs.pm/phoenix_storybook/PhoenixStorybook.#{page}.html"
- end
-end
diff --git a/test/lightning_web/live/workflow_live/ai_assistant_component_test.exs b/test/lightning_web/live/workflow_live/ai_assistant_component_test.exs
index 3e67d210df5..db1f3f4d6c8 100644
--- a/test/lightning_web/live/workflow_live/ai_assistant_component_test.exs
+++ b/test/lightning_web/live/workflow_live/ai_assistant_component_test.exs
@@ -197,9 +197,91 @@ defmodule LightningWeb.WorkflowLive.AiAssistant.ComponentTest do
code_elements = Floki.find(parsed_html, "code")
assert length(code_elements) > 0
- # The code element should have the language as its class
+ # MDEx emits the standard `language-` prefix on fenced code blocks
code_element = hd(code_elements)
- assert Floki.attribute(code_element, "class") == ["javascript"]
+ assert Floki.attribute(code_element, "class") == ["language-javascript"]
+ end
+
+ test "applies styling classes to standard markdown elements" do
+ content = """
+ # Title
+
+ ## Subtitle
+
+ Some paragraph text.
+
+ - bullet one
+ - bullet two
+
+ 1. step one
+ 2. step two
+ """
+
+ html =
+ render_component(&AiAssistant.Component.formatted_content/1,
+ id: "formatted-content",
+ content: content
+ )
+
+ parsed_html = Floki.parse_document!(html)
+
+ assert Floki.attribute(Floki.find(parsed_html, "h1"), "class") == [
+ "text-2xl font-bold mb-6"
+ ]
+
+ assert Floki.attribute(Floki.find(parsed_html, "h2"), "class") == [
+ "text-xl font-semibold mb-4 mt-8"
+ ]
+
+ assert Floki.attribute(Floki.find(parsed_html, "ul"), "class") == [
+ "list-disc pl-8 space-y-1"
+ ]
+
+ assert Floki.attribute(Floki.find(parsed_html, "ol"), "class") == [
+ "list-decimal pl-8 space-y-1"
+ ]
+
+ assert "mt-1 mb-2 text-gray-800" in Floki.attribute(
+ Floki.find(parsed_html, "p"),
+ "class"
+ )
+
+ assert Enum.all?(
+ Floki.find(parsed_html, "li"),
+ &(Floki.attribute(&1, "class") == ["text-gray-800"])
+ )
+ end
+
+ test "renders GFM extensions (tables, strikethrough, autolinks)" do
+ content = """
+ | Name | Role |
+ |------|------|
+ | Ada | Dev |
+
+ ~~deprecated~~ and see https://example.com for details.
+ """
+
+ html =
+ render_component(&AiAssistant.Component.formatted_content/1,
+ id: "formatted-content",
+ content: content
+ )
+
+ parsed_html = Floki.parse_document!(html)
+
+ # GFM tables render as a real table, not raw pipe text
+ assert Floki.find(parsed_html, "table") != []
+ assert Floki.find(parsed_html, "th") |> Floki.text() =~ "Name"
+ assert Floki.find(parsed_html, "td") |> Floki.text() =~ "Ada"
+
+ # Strikethrough
+ assert Floki.find(parsed_html, "del") |> Floki.text() == "deprecated"
+
+ # Bare URL is autolinked
+ assert Enum.any?(
+ Floki.find(parsed_html, "a"),
+ &(Floki.attribute(&1, "href") == ["https://example.com"])
+ )
end
end