defmodule BDS.UI.CodeEditor.Highlight do @moduledoc """ Syntax highlighting for the TUI code editor (issue #32). Wraps `ExRatatui.CodeBlock.highlight/3` with the editor's language set: base lexing via syntect, plus pure-Elixir overlays for the syntaxes syntect does not know — post macros (`[[gallery names="x"]]`) over the markdown base and Liquid tags (`{{ … }}` / `{% … %}`) over the HTML base. Mirrors the GUI Monaco language registrations in `assets/js/monaco/languages.js`. """ alias BDS.UI.CodeEditor.Overlay alias ExRatatui.Text.Line alias ExRatatui.Widgets.CodeBlock @type language :: :markdown_macros | :liquid | :lua | :text @doc """ Highlights `content` for `language` using `theme`, returning one `%ExRatatui.Text.Line{}` per source line. The returned line count always matches `String.split(content, "\\n")` (syntect drops trailing empty lines, but the textarea's line count is authoritative for the cursor row, so they are padded back). Span contents never contain newline characters. """ @spec highlight(String.t(), language(), CodeBlock.theme()) :: [Line.t()] def highlight(content, language, theme) when is_binary(content) do content |> base_highlight(language, theme) |> sanitize_lines() |> pad_lines(source_line_count(content)) |> apply_overlay(language) end defp base_highlight(content, :markdown_macros, theme), do: ExRatatui.CodeBlock.highlight(content, "markdown", theme) defp base_highlight(content, :liquid, theme), do: ExRatatui.CodeBlock.highlight(content, "html", theme) defp base_highlight(content, :lua, theme), do: ExRatatui.CodeBlock.highlight(content, "lua", theme) defp base_highlight(content, :text, theme), do: ExRatatui.CodeBlock.highlight(content, nil, theme) # syntect appends the line terminator to the last span of a line; # spans fed to `ExRatatui.Widgets.Paragraph` must not contain "\n". defp sanitize_lines(lines) do Enum.map(lines, fn %Line{spans: spans} = line -> spans = spans |> Enum.map(&%{&1 | content: String.replace(&1.content, ~r/[\r\n]/, "")}) |> Enum.reject(&(&1.content == "")) %{line | spans: spans} end) end defp source_line_count(content), do: content |> String.split("\n") |> length() defp pad_lines(lines, count) when length(lines) >= count, do: lines defp pad_lines(lines, count), do: lines ++ List.duplicate(%Line{}, count - length(lines)) defp apply_overlay(lines, :markdown_macros), do: Enum.map(lines, &Overlay.markdown_macros/1) defp apply_overlay(lines, :liquid), do: Enum.map(lines, &Overlay.liquid/1) defp apply_overlay(lines, _language), do: lines end