Skip to content

Commit 061724c

Browse files
authored
feat: add sigil support for module attribute formatting (#9)
Adds comprehensive support for formatting markdown content within ~S and ~s sigils in module attributes with all delimiter types and proper formatting preservation.
1 parent f89e47d commit 061724c

File tree

6 files changed

+1017
-3
lines changed

6 files changed

+1017
-3
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ Add to your `mix.exs`:
2525
```elixir
2626
def deps do
2727
[
28-
{:dprint_markdown_formatter, "~> 0.4.0"}
28+
{:dprint_markdown_formatter, "~> 0.5.0"}
2929
]
3030
end
3131
```

lib/dprint_markdown_formatter/ast_processor.ex

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,27 @@ defmodule DprintMarkdownFormatter.AstProcessor do
9595
acc
9696
)
9797

98+
# Handle sigil patterns: @moduledoc ~S"""content""" or @moduledoc ~s/content/
99+
{:@, _meta,
100+
[
101+
{attr, _attr_meta,
102+
[
103+
{sigil_type, sigil_meta,
104+
[{:<<>>, _binary_meta, [doc_content]} = binary_node, _modifiers]} =
105+
sigil_node
106+
]}
107+
]}
108+
when is_atom(attr) and is_binary(doc_content) and sigil_type in [:sigil_S, :sigil_s] ->
109+
process_sigil_doc_attribute(
110+
node,
111+
attr,
112+
doc_content,
113+
%{sigil: sigil_node, binary: binary_node, type: sigil_type, meta: sigil_meta},
114+
doc_attributes,
115+
nif_config,
116+
acc
117+
)
118+
98119
_other_node ->
99120
{node, acc}
100121
end
@@ -148,6 +169,67 @@ defmodule DprintMarkdownFormatter.AstProcessor do
148169
end
149170
end
150171

172+
defp process_sigil_doc_attribute(
173+
node,
174+
attr,
175+
doc_content,
176+
%{sigil: sigil_node, binary: binary_node, type: sigil_type, meta: sigil_meta},
177+
doc_attributes,
178+
nif_config,
179+
acc
180+
) do
181+
if attr in doc_attributes do
182+
case format_markdown_content(doc_content, nif_config) do
183+
{:ok, formatted} when formatted != doc_content ->
184+
create_patch_for_formatted_sigil_content(
185+
node,
186+
formatted,
187+
sigil_node,
188+
binary_node,
189+
sigil_type,
190+
sigil_meta,
191+
acc
192+
)
193+
194+
{:ok, _unchanged} ->
195+
{node, acc}
196+
197+
{:error, _error} ->
198+
{node, acc}
199+
end
200+
else
201+
{node, acc}
202+
end
203+
end
204+
205+
defp create_patch_for_formatted_sigil_content(
206+
node,
207+
formatted,
208+
sigil_node,
209+
_binary_node,
210+
sigil_type,
211+
sigil_meta,
212+
acc
213+
) do
214+
range = Sourceror.get_range(sigil_node)
215+
delimiter = sigil_meta[:delimiter]
216+
217+
sigil_prefix =
218+
case sigil_type do
219+
:sigil_S -> "~S"
220+
:sigil_s -> "~s"
221+
end
222+
223+
replacement = PatchBuilder.build_replacement_sigil_string(formatted, sigil_prefix, delimiter)
224+
225+
patch = %Sourceror.Patch{
226+
range: range,
227+
change: replacement
228+
}
229+
230+
{node, [patch | acc]}
231+
end
232+
151233
defp format_markdown_content(content, nif_config) do
152234
case DprintMarkdownFormatter.Native.format_markdown(content, nif_config) do
153235
{:ok, formatted} ->

lib/dprint_markdown_formatter/patch_builder.ex

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ defmodule DprintMarkdownFormatter.PatchBuilder do
66
replacements, supporting both simple strings and heredoc formats.
77
"""
88

9+
@single_char_delimiters ["\"", "'", "/", "|", "(", "[", "{", "<"]
10+
911
@doc """
1012
Builds a replacement string for formatted content based on the original
1113
delimiter.
@@ -76,4 +78,68 @@ defmodule DprintMarkdownFormatter.PatchBuilder do
7678
# Heredoc format - preserve as heredoc
7779
"\"\"\"\n#{formatted}\n\"\"\""
7880
end
81+
82+
@doc """
83+
Builds a replacement string for sigil content, preserving the original sigil
84+
type and delimiter.
85+
86+
Handles all supported sigil delimiters and maintains proper formatting for both
87+
simple and heredoc-style sigils.
88+
89+
## Examples
90+
91+
iex> DprintMarkdownFormatter.PatchBuilder.build_replacement_sigil_string("Hello", "~S", "\\"")
92+
"~S\\"Hello\\""
93+
94+
iex> DprintMarkdownFormatter.PatchBuilder.build_replacement_sigil_string("Hello\\nWorld", "~S", "\\"\\"\\\"")
95+
"~S\\"\\"\\\"\\nHello\\nWorld\\n\\"\\"\\""
96+
97+
iex> DprintMarkdownFormatter.PatchBuilder.build_replacement_sigil_string("Hello", "~s", "/")
98+
"~s/Hello/"
99+
"""
100+
@spec build_replacement_sigil_string(String.t(), String.t(), String.t()) :: String.t()
101+
def build_replacement_sigil_string(formatted, sigil_prefix, delimiter) do
102+
case delimiter do
103+
"\"\"\"" ->
104+
# Heredoc sigil - format exactly like regular heredocs
105+
"#{sigil_prefix}\"\"\"\n#{formatted}\n\"\"\"\n"
106+
107+
"'''" ->
108+
# Triple single quote heredoc - same as triple double quotes
109+
"#{sigil_prefix}'''\n#{formatted}\n'''\n"
110+
111+
single_delimiter when single_delimiter in @single_char_delimiters ->
112+
# Single character delimiters - preserve original delimiter structure
113+
closing_delimiter = get_closing_delimiter(single_delimiter)
114+
115+
# For simple delimiters, match the expected format precisely
116+
# Single-line sigils should not have trailing newline
117+
# Multi-line sigils should have newline after closing delimiter
118+
has_newlines = String.contains?(formatted, "\n")
119+
120+
if has_newlines do
121+
# Multi-line content needs newline after closing delimiter
122+
"#{sigil_prefix}#{single_delimiter}\n#{formatted}\n#{closing_delimiter}\n"
123+
else
124+
# Single-line content - no trailing newline
125+
"#{sigil_prefix}#{single_delimiter}#{formatted}#{closing_delimiter}"
126+
end
127+
128+
_unknown_delimiter ->
129+
# Fallback to heredoc for unknown delimiters
130+
"#{sigil_prefix}\"\"\"\n#{formatted}\n\"\"\"\n"
131+
end
132+
end
133+
134+
# Private helper to get the closing delimiter for bracket-style delimiters
135+
defp get_closing_delimiter(opening) do
136+
case opening do
137+
"(" -> ")"
138+
"[" -> "]"
139+
"{" -> "}"
140+
"<" -> ">"
141+
# For quotes, slashes, pipes - closing is same as opening
142+
other -> other
143+
end
144+
end
79145
end

llms.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Add to `mix.exs`:
2121
```elixir
2222
def deps do
2323
[
24-
{:dprint_markdown_formatter, "~> 0.4.0"}
24+
{:dprint_markdown_formatter, "~> 0.5.0"}
2525
]
2626
end
2727
```

mix.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
defmodule DprintMarkdownFormatter.MixProject do
22
use Mix.Project
33

4-
@version "0.4.0"
4+
@version "0.5.0"
55
@source_url "https://github.com/fahchen/dprint_markdown_formatter"
66

77
def project do

0 commit comments

Comments
 (0)