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 %> -
-
- {dt} -
-
- - {link} - -
-
- <% 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