From d66465556f0b749d80044dda878baa05b4b07e94 Mon Sep 17 00:00:00 2001 From: "Tj (bougyman) Vanderpoel" Date: Fri, 21 Aug 2026 08:38:14 -0400 Subject: [PATCH] feat: add syntax highlighting to markdown renderer --- .github/workflows/main.yaml | 22 ++++++++-------- app/lib/linear_cli/cli/commands.ex | 13 +++++++--- app/mix.exs | 1 + app/mix.lock | 2 ++ app/test/linear_cli/cli/display_test.exs | 32 ++++++++++++++++++++++++ 5 files changed, 56 insertions(+), 14 deletions(-) create mode 100644 app/test/linear_cli/cli/display_test.exs diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index f4d74f6..47e7be2 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -168,13 +168,13 @@ jobs: fi - # Both Linux targets build on native matching runners. `lc version` - # boots the application and performs a hidden Marcli render, forcing - # mdex_native and its NIF to load before the command can exit 0. No API - # key or network is needed. macOS/Windows runtime smoke tests remain - # out of scope because their hosted runners are not all native to the - # configured Burrito targets. + # boots the application and performs a hidden syntax-highlighted Marcli + # render, forcing mdex_native, its NIF, Makeup, and the Elixir lexer to + # load before the command can exit 0. No API key or network is needed. + # macOS/Windows runtime smoke tests remain out of scope because their + # hosted runners are not all native to the configured Burrito targets. if: startsWith(matrix.target, 'linux_') - name: Smoke-test the ${{ matrix.target }} binary and Markdown NIF + name: Smoke-test the ${{ matrix.target }} binary and Markdown runtime timeout-minutes: 1 run: ./burrito_out/lc_${{ matrix.target }} version - @@ -376,16 +376,16 @@ jobs: run: ./ci/build_image.sh "${{ needs.burrito-package.outputs.tag_name }}" - # Verify the musl NIF actually loads on Alpine before publishing. - # `lc version` boots the OTP app and performs a hidden Markdown render, - # forcing mdex_native's NIF and its bundled libgcc runtime to load. - # It then prints the version and exits 0; no API key or network is - # needed. ci/build_image.sh prefers + # `lc version` boots the OTP app and performs a hidden syntax-highlighted + # Markdown render, forcing mdex_native's NIF, its bundled libgcc runtime, + # Makeup, and the Elixir lexer to load. It then prints the version and + # exits 0; no API key or network is needed. ci/build_image.sh prefers # Podman, so the image lives in Podman's local storage; we run it # directly rather than loading a tarball. LINEAR_CLI_DAEMON is # overridden to false (the image bakes in true so the default CMD # starts the daemon; without this override the app ignores `version` # and stays alive as a daemon, timing out the step). See EXT-7. - name: Smoke-test the container image boots and NIFs load on Alpine/musl + name: Smoke-test the container image Markdown runtime on Alpine/musl timeout-minutes: 1 run: | podman run --rm -e LINEAR_CLI_DAEMON=false "linear-cli:${{ needs.burrito-package.outputs.tag_name }}" lc version diff --git a/app/lib/linear_cli/cli/commands.ex b/app/lib/linear_cli/cli/commands.ex index 120eba4..b9f25e3 100644 --- a/app/lib/linear_cli/cli/commands.ex +++ b/app/lib/linear_cli/cli/commands.ex @@ -19,8 +19,8 @@ defmodule LinearCli.CLI.Commands do Ported from commands/version.rb, extended to respect the global `--output json` option like every other command does - previously ignored it and always printed plain text. The hidden Markdown render makes - this command a complete release smoke test for mdex_native's NIF as well as - the application boot path. + this command a complete release smoke test for mdex_native's NIF, Marcli's + syntax-highlighting lexer, and the application boot path. """ def version(%{options: options}) do verify_markdown_runtime!() @@ -36,7 +36,14 @@ defmodule LinearCli.CLI.Commands do end defp verify_markdown_runtime! do - _rendered = Marcli.render("runtime check", escape_sequences: false) + theme = Marcli.Theme.default() + rendered = Marcli.render("```elixir\ndef smoke, do: :ok\n```") + highlighted_keyword = theme.syntax.keyword_declaration <> "def" <> theme.reset + + unless String.contains?(rendered, highlighted_keyword) do + raise "Markdown syntax-highlighting runtime is unavailable" + end + :ok end diff --git a/app/mix.exs b/app/mix.exs index 646b62a..30b995b 100644 --- a/app/mix.exs +++ b/app/mix.exs @@ -61,6 +61,7 @@ defmodule LinearCli.MixProject do defp deps do [ {:marcli, "~> 0.3"}, + {:makeup_elixir, "~> 1.0"}, {:owl, "~> 0.13"}, {:optimus, "~> 0.6"}, {:req, "~> 0.7"}, diff --git a/app/mix.lock b/app/mix.lock index f7c99e8..812af6c 100644 --- a/app/mix.lock +++ b/app/mix.lock @@ -22,6 +22,8 @@ "igniter": {:hex, :igniter, "0.8.3", "9de74d3885efae43b0b58dc6f7b816963c4bbd391e6b6fe6922ee21c4e384c76", [:mix], [{:ex_ast, "~> 0.5", [hex: :ex_ast, repo: "hexpm", optional: false]}, {:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4.5", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "afc5e3848d885e680da5c3b65e5e7717555a08cd12305190ff2be76427af39ff"}, "iterex": {:hex, :iterex, "0.1.2", "58f9b9b9a22a55cbfc7b5234a9c9c63eaac26d276b3db80936c0e1c60355a5a6", [:mix], [], "hexpm", "2e103b8bcc81757a9af121f6dc0df312c9a17220f302b1193ef720460d03029d"}, "jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"}, + "makeup": {:hex, :makeup, "1.2.2", "882d46dc0905e9ff7abf2aab61a7e6b3dcc555533977d8a23b06019e6c89ac94", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "9a1a24e5b343b8ae16abea0822c10a6f75da27af7fa802ada5251f7579bfccfa"}, + "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"}, "marcli": {:hex, :marcli, "0.3.1", "183d2c674e416cdd90a5b395d60d573ad5d4381ec18239de2b2cfa249d926d7c", [:mix], [{:color, "~> 0.3", [hex: :color, repo: "hexpm", optional: true]}, {:makeup, "~> 1.2", [hex: :makeup, repo: "hexpm", optional: true]}, {:mdex, "~> 0.11", [hex: :mdex, repo: "hexpm", optional: false]}], "hexpm", "731383f9b9bdfd13affe2d43a3cef441440c4a9182cb59724478e51b85e2c7d4"}, "mdex": {:hex, :mdex, "0.13.5", "c1c94d230ccaab01ad0c68090d3b31613c10ece1844f32b55895da4ce0c63029", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:lumis, "~> 0.1", [hex: :lumis, repo: "hexpm", optional: true]}, {:mdex_native, ">= 0.2.6", [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, "~> 0.20.0 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}], "hexpm", "c57409fb6b34fbc58fbce0a6da670c9a4b5a2e94f86abdc56e9e213ed74620f2"}, "mdex_native": {:hex, :mdex_native, "0.2.7", "46203ee9c4fe2c94feef26e4d976235ad022b4770d3d307fa6b445d293b2571f", [:mix], [{:rustler, "~> 0.32", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "05efa68775644ef6be6109f76c1f5f8778ff7e7ed9fe4a03541758158172bc77"}, diff --git a/app/test/linear_cli/cli/display_test.exs b/app/test/linear_cli/cli/display_test.exs new file mode 100644 index 0000000..5c65d94 --- /dev/null +++ b/app/test/linear_cli/cli/display_test.exs @@ -0,0 +1,32 @@ +defmodule LinearCli.CLI.DisplayTest do + use ExUnit.Case, async: true + + import ExUnit.CaptureIO + + alias LinearCli.CLI.Display + alias LinearCli.Linear.Issue + + test "full issue output syntax-highlights fenced Elixir code" do + issue = %Issue{ + id: "issue-1", + identifier: "EXT-1", + title: "Highlight markdown", + description: """ + ```elixir + defmodule Example do + def answer, do: 42 + end + ``` + """, + comments: [] + } + + output = capture_io(fn -> Display.show(issue, %{full: true}) end) + theme = Marcli.Theme.default() + + assert output =~ theme.syntax.keyword_declaration <> "defmodule" <> theme.reset + assert output =~ theme.syntax.name_class <> "Example" <> theme.reset + assert output =~ theme.syntax.number <> "42" <> theme.reset + refute output =~ theme.code_text <> "defmodule" + end +end