Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 41 additions & 118 deletions assets/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@
#splash .frame-path:nth-child(6) { animation-delay: 1s; }
#splash .frame-path:nth-child(7) { animation-delay: 1.2s; }

/* Syntax highlighting for code blocks (Makeup) */
/* Syntax highlighting for code blocks (Lumis/MDEx) */
pre.highlight {
background-color: #e6eaf0;
border-radius: 6px;
Expand All @@ -249,122 +249,45 @@ html.dark pre.highlight {
}

/* Light mode token styles */
.highlight .unselectable { user-select: none; }
.highlight .hll { background-color: #ffffcc; }
.highlight .c { color: #6a737d; font-style: italic; }
.highlight .k { color: #d73a49; font-weight: bold; }
.highlight .o { color: #d73a49; }
.highlight .cm { color: #6a737d; font-style: italic; }
.highlight .cp { color: #d73a49; font-weight: bold; }
.highlight .c1 { color: #6a737d; font-style: italic; }
.highlight .cs { color: #6a737d; font-weight: bold; font-style: italic; }
.highlight .gd { color: #b31d28; background-color: #ffeef0; }
.highlight .ge { font-style: italic; }
.highlight .gr { color: #b31d28; }
.highlight .gh { color: #005cc5; font-weight: bold; }
.highlight .gi { color: #22863a; background-color: #f0fff4; }
.highlight .go { color: #6a737d; }
.highlight .gp { color: #005cc5; font-weight: bold; }
.highlight .gs { font-weight: bold; }
.highlight .gu { color: #6f42c1; font-weight: bold; }
.highlight .gt { color: #0044DD; }
.highlight .kc { color: #005cc5; font-weight: bold; }
.highlight .kd { color: #d73a49; font-weight: bold; }
.highlight .kn { color: #d73a49; font-weight: bold; }
.highlight .kp { color: #d73a49; font-weight: bold; }
.highlight .kr { color: #d73a49; font-weight: bold; }
.highlight .kt { color: #d73a49; font-weight: bold; }
.highlight .m { color: #005cc5; }
.highlight .s { color: #032f62; }
.highlight .na { color: #005cc5; }
.highlight .nb { color: #005cc5; }
.highlight .nc { color: #6f42c1; font-weight: bold; }
.highlight .no { color: #005cc5; }
.highlight .nd { color: #6f42c1; }
.highlight .ni { color: #005cc5; }
.highlight .ne { color: #005cc5; font-weight: bold; }
.highlight .nf { color: #6f42c1; }
.highlight .nl { color: #005cc5; font-weight: bold; }
.highlight .nn { color: #6f42c1; }
.highlight .nt { color: #22863a; }
.highlight .nv { color: #24292e; }
.highlight .ow { color: #d73a49; font-weight: bold; }
.highlight .w { color: #6a737d; }
.highlight .mf { color: #005cc5; }
.highlight .mh { color: #005cc5; }
.highlight .mi { color: #005cc5; }
.highlight .mo { color: #005cc5; }
.highlight .sb { color: #032f62; }
.highlight .sc { color: #032f62; }
.highlight .sd { color: #032f62; }
.highlight .s2 { color: #032f62; }
.highlight .se { color: #032f62; }
.highlight .sh { color: #032f62; }
.highlight .si { color: #005cc5; }
.highlight .sx { color: #032f62; }
.highlight .sr { color: #032f62; }
.highlight .s1 { color: #032f62; }
.highlight .ss { color: #005cc5; }
.highlight .bp { color: #005cc5; }
.highlight .vc { color: #24292e; }
.highlight .vg { color: #24292e; }
.highlight .vi { color: #24292e; }
.highlight .il { color: #005cc5; }
.highlight .keyword { color: #d73a49; font-weight: bold; }
.highlight .keyword-operator { color: #d73a49; font-weight: bold; }
.highlight .module { color: #6f42c1; font-weight: bold; }
.highlight .function { color: #6f42c1; }
.highlight .function-call { color: #6f42c1; }
.highlight .string { color: #032f62; }
.highlight .string-special { color: #032f62; }
.highlight .string-special-symbol { color: #005cc5; }
.highlight .number { color: #005cc5; }
.highlight .number-float { color: #005cc5; }
.highlight .comment { color: #6a737d; font-style: italic; }
.highlight .operator { color: #d73a49; }
.highlight .variable { color: #24292e; }
.highlight .constant { color: #005cc5; }
.highlight .constant-builtin { color: #005cc5; font-weight: bold; }
.highlight .boolean { color: #005cc5; font-weight: bold; }
.highlight .punctuation-bracket { color: #24292e; }
.highlight .punctuation-delimiter { color: #24292e; }
.highlight .punctuation-special { color: #d73a49; }
.highlight .text { color: #24292e; }

/* Dark mode token styles */
html.dark .highlight .c { color: #8b949e; }
html.dark .highlight .k { color: #ff7b72; }
html.dark .highlight .o { color: #ff7b72; }
html.dark .highlight .cm { color: #8b949e; }
html.dark .highlight .cp { color: #ff7b72; }
html.dark .highlight .c1 { color: #8b949e; }
html.dark .highlight .cs { color: #8b949e; }
html.dark .highlight .gd { color: #ffa198; background-color: #490202; }
html.dark .highlight .gr { color: #ffa198; }
html.dark .highlight .gh { color: #79c0ff; }
html.dark .highlight .gi { color: #56d364; background-color: #0f5323; }
html.dark .highlight .go { color: #8b949e; }
html.dark .highlight .gp { color: #79c0ff; }
html.dark .highlight .gu { color: #d2a8ff; }
html.dark .highlight .kc { color: #79c0ff; }
html.dark .highlight .kd { color: #ff7b72; }
html.dark .highlight .kn { color: #ff7b72; }
html.dark .highlight .kp { color: #ff7b72; }
html.dark .highlight .kr { color: #ff7b72; }
html.dark .highlight .kt { color: #ff7b72; }
html.dark .highlight .m { color: #79c0ff; }
html.dark .highlight .s { color: #a5d6ff; }
html.dark .highlight .na { color: #79c0ff; }
html.dark .highlight .nb { color: #79c0ff; }
html.dark .highlight .nc { color: #d2a8ff; }
html.dark .highlight .no { color: #79c0ff; }
html.dark .highlight .nd { color: #d2a8ff; }
html.dark .highlight .ni { color: #79c0ff; }
html.dark .highlight .ne { color: #79c0ff; }
html.dark .highlight .nf { color: #d2a8ff; }
html.dark .highlight .nl { color: #79c0ff; }
html.dark .highlight .nn { color: #d2a8ff; }
html.dark .highlight .nt { color: #7ee787; }
html.dark .highlight .nv { color: #c9d1d9; }
html.dark .highlight .ow { color: #ff7b72; }
html.dark .highlight .w { color: #8b949e; }
html.dark .highlight .mf { color: #79c0ff; }
html.dark .highlight .mh { color: #79c0ff; }
html.dark .highlight .mi { color: #79c0ff; }
html.dark .highlight .mo { color: #79c0ff; }
html.dark .highlight .sb { color: #a5d6ff; }
html.dark .highlight .sc { color: #a5d6ff; }
html.dark .highlight .sd { color: #a5d6ff; }
html.dark .highlight .s2 { color: #a5d6ff; }
html.dark .highlight .se { color: #a5d6ff; }
html.dark .highlight .sh { color: #a5d6ff; }
html.dark .highlight .si { color: #79c0ff; }
html.dark .highlight .sx { color: #a5d6ff; }
html.dark .highlight .sr { color: #a5d6ff; }
html.dark .highlight .s1 { color: #a5d6ff; }
html.dark .highlight .ss { color: #79c0ff; }
html.dark .highlight .bp { color: #79c0ff; }
html.dark .highlight .vc { color: #c9d1d9; }
html.dark .highlight .vg { color: #c9d1d9; }
html.dark .highlight .vi { color: #c9d1d9; }
html.dark .highlight .il { color: #79c0ff; }
html.dark .highlight .keyword { color: #ff7b72; }
html.dark .highlight .keyword-operator { color: #ff7b72; }
html.dark .highlight .module { color: #d2a8ff; }
html.dark .highlight .function { color: #d2a8ff; }
html.dark .highlight .function-call { color: #d2a8ff; }
html.dark .highlight .string { color: #a5d6ff; }
html.dark .highlight .string-special { color: #a5d6ff; }
html.dark .highlight .string-special-symbol { color: #79c0ff; }
html.dark .highlight .number { color: #79c0ff; }
html.dark .highlight .number-float { color: #79c0ff; }
html.dark .highlight .comment { color: #8b949e; }
html.dark .highlight .operator { color: #ff7b72; }
html.dark .highlight .variable { color: #c9d1d9; }
html.dark .highlight .constant { color: #79c0ff; }
html.dark .highlight .constant-builtin { color: #79c0ff; }
html.dark .highlight .boolean { color: #79c0ff; }
html.dark .highlight .punctuation-bracket { color: #c9d1d9; }
html.dark .highlight .punctuation-delimiter { color: #c9d1d9; }
html.dark .highlight .punctuation-special { color: #ff7b72; }
html.dark .highlight .text { color: #c9d1d9; }
29 changes: 0 additions & 29 deletions lib/clarity/components/code_highlighter.ex

This file was deleted.

114 changes: 41 additions & 73 deletions lib/clarity/components/markdown_component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@

use Phoenix.Component

alias Clarity.Components.CodeHighlighter
alias Clarity.Perspective.Lens
alias Phoenix.LiveView.Rendered
alias Phoenix.LiveView.Socket

@mdex_opts [
extension: [table: true, autolink: true, strikethrough: true],
syntax_highlight: [formatter: {:html_linked, pre_class: "highlight"}]
]

attr :content, :any, required: true, doc: "The markdown content to render"
attr :prefix, :string, required: true, doc: "URL prefix for link generation"
attr :lens, Lens, required: true, doc: "Current lens for link generation"
Expand Down Expand Up @@ -41,92 +45,56 @@
|> Phoenix.HTML.raw()
end

@dialyzer {:nowarn_function, parse_and_transform_markdown: 3}
@spec parse_and_transform_markdown(
markdown :: String.t(),
prefix :: String.t(),
lens :: Lens.t()
) :: String.t()
defp parse_and_transform_markdown(markdown, prefix, lens) do
case Earmark.Parser.as_ast(markdown) do
{:ok, ast, _messages} ->
ast
|> Earmark.Transform.map_ast(&transform_vertex_links(&1, prefix, lens))
|> Earmark.Transform.map_ast(&transform_code_blocks/1)
|> Earmark.Transform.transform()

{:error, _reason, _messages} ->
case MDEx.parse_document(markdown, @mdex_opts) do
{:ok, doc} ->
doc
|> MDEx.traverse_and_update(&transform_vertex_links(&1, prefix, lens))
|> MDEx.to_html!(@mdex_opts)

{:error, _reason} ->
fallback_render(markdown)
end
end

@spec transform_vertex_links(
ast_node :: Earmark.Parser.ast_node(),
prefix :: String.t(),
lens :: Lens.t()
) ::
Earmark.Parser.ast_node()
defp transform_vertex_links({"a", attrs, content, meta} = node, prefix, lens) do
case Enum.find(attrs, fn {key, _value} -> key == "href" end) do
{"href", "vertex://" <> vertex_path} ->
new_href = build_clarity_path(vertex_path, prefix, lens)

new_attrs =
attrs
|> Enum.reject(fn {key, _value} -> key == "href" end)
|> Enum.concat([
{"href", new_href},
{"data-phx-link", "patch"},
{"data-phx-link-state", "push"}
])

{"a", new_attrs, content, meta}

_other ->
node
@spec transform_vertex_links(MDEx.Document.md_node(), String.t(), Lens.t()) :: MDEx.Document.md_node()
defp transform_vertex_links(%{nodes: children} = parent, prefix, lens) when is_list(children) do
if Enum.any?(children, &vertex_link?/1) do
%{parent | nodes: rewrite_vertex_links(children, prefix, lens)}
else
parent
end
end

defp transform_vertex_links(node, _prefix, _lens), do: node

@spec transform_code_blocks(ast_node :: Earmark.Parser.ast_node()) ::
Earmark.Parser.ast_node() | {:replace, Earmark.Parser.ast_node()}
defp transform_code_blocks(
{"pre", pre_attrs, [{"code", code_attrs, [content], code_meta}], pre_meta} = node
) do
language = extract_language(code_attrs)

case CodeHighlighter.get_lexer(language) do
nil ->
node

lexer ->
highlighted_spans = Makeup.highlight_inner_html(content, lexer: lexer)
code_meta_with_verbatim = Map.put(code_meta, :verbatim, true)
pre_meta_with_verbatim = Map.put(pre_meta, :verbatim, true)

new_node =
{"pre", [{"class", "highlight"} | pre_attrs],
[{"code", code_attrs, [highlighted_spans], code_meta_with_verbatim}],
pre_meta_with_verbatim}

{:replace, new_node}
end
defp vertex_link?(%MDEx.Link{url: "vertex://" <> _}), do: true

Check warning on line 76 in lib/clarity/components/markdown_component.ex

View workflow job for this annotation

GitHub Actions / ash-ci / mix credo --strict

Functions should have a @SPEC type specification.
defp vertex_link?(_), do: false

Check warning on line 77 in lib/clarity/components/markdown_component.ex

View workflow job for this annotation

GitHub Actions / ash-ci / mix credo --strict

Functions should have a @SPEC type specification.

# MDEx.Link has no HTML attributes field; use Raw nodes for data-phx-link attrs.
@spec rewrite_vertex_links([MDEx.Document.md_node()], String.t(), Lens.t()) ::
[MDEx.Document.md_node()]
defp rewrite_vertex_links(children, prefix, lens) do
Enum.flat_map(children, fn
%MDEx.Link{url: "vertex://" <> vertex_path, nodes: link_children} ->
vertex_path
|> build_clarity_path(prefix, lens)
|> raw_phx_link(link_children)

other ->
[other]
end)
end

defp transform_code_blocks(node), do: node

@spec extract_language(attrs :: list()) :: String.t() | nil
defp extract_language(attrs) do
case Enum.find(attrs, fn {key, _value} -> key == "class" end) do
{"class", class_value} ->
class_value
|> String.split()
|> Enum.find(&(&1 not in ["", "inline"]))

_other ->
nil
end
defp raw_phx_link(url, children) do

Check warning on line 94 in lib/clarity/components/markdown_component.ex

View workflow job for this annotation

GitHub Actions / ash-ci / mix credo --strict

Functions should have a @SPEC type specification.
open = %MDEx.Raw{literal: ~s(<a href="#{url}" data-phx-link="patch" data-phx-link-state="push">)}
close = %MDEx.Raw{literal: "</a>"}
[open | children] ++ [close]
end

@spec build_clarity_path(
Expand All @@ -140,9 +108,9 @@

@spec fallback_render(markdown :: String.t()) :: String.t()
defp fallback_render(markdown) do
case Earmark.as_html(markdown) do
{:ok, html, _messages} -> html
{:error, reason, _messages} -> "<p>Error rendering markdown: #{reason}</p>"
case MDEx.to_html(markdown, @mdex_opts) do
{:ok, html} -> html
{:error, _reason} -> "<p>Error rendering markdown</p>"
end
end
end
3 changes: 1 addition & 2 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,7 @@ defmodule Clarity.MixProject do
{:phoenix, "~> 1.8"},
{:phoenix_html, "~> 4.2"},
{:phoenix_live_view, "~> 1.0"},
{:earmark, "~> 1.4"},
{:makeup_elixir, "~> 1.0"},
{:mdex, "~> 0.11.6"},
{:telemetry, "~> 1.3"},
{:telemetry_registry, "~> 0.3"},
{:igniter, "~> 0.6", optional: true},
Expand Down
Loading
Loading