feat: issue #32 markdown editor for TUI
This commit is contained in:
72
lib/bds/ui/code_editor/highlight.ex
Normal file
72
lib/bds/ui/code_editor/highlight.ex
Normal file
@@ -0,0 +1,72 @@
|
||||
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
|
||||
Reference in New Issue
Block a user