Skip to content

Commit be12c00

Browse files
authored
Merge pull request #2 from olivermt/ash-cms-source-extraction-pr
Add streaming candidate extraction
2 parents 9dd4165 + f144ee6 commit be12c00

7 files changed

Lines changed: 702 additions & 16 deletions

File tree

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,23 @@ This requires [Zig 0.15.2+](https://ziglang.org/download/).
7373
TailwindCompiler.compile(["flex", "p-4", "hover:bg-blue-500/50", "sm:text-lg"])
7474
#=> {:ok, ".flex{display:flex}.p-4{padding:calc(var(--spacing)*4)}..."}
7575

76+
# Extract candidate strings from raw HTML/template source
77+
TailwindCompiler.candidates(~s(<div class="flex p-4 hover:bg-blue-500/50"></div>))
78+
#=> ["div", "flex", "p-4", "hover:bg-blue-500/50", "/div"]
79+
80+
# Extract candidates lazily from a file or IO stream
81+
File.stream!("index.html", [], :line)
82+
|> TailwindCompiler.candidates()
83+
|> Enum.to_list()
84+
85+
# Compile raw HTML/template source by extracting candidates first
86+
TailwindCompiler.compile_source(~s(<div class="flex p-4"></div>))
87+
#=> {:ok, ".flex{display:flex}.p-4{padding:calc(var(--spacing)*4)}..."}
88+
89+
# compile_source/2 also accepts streams
90+
File.stream!("index.html", [], :line)
91+
|> TailwindCompiler.compile_source(preflight: false)
92+
7693
# Without preflight (base CSS reset)
7794
TailwindCompiler.compile(["flex", "hidden"], preflight: false)
7895

lib/tailwind_compiler.ex

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,66 @@ defmodule TailwindCompiler do
1212
1313
"""
1414

15+
@doc """
16+
Extract Tailwind candidate strings from source text.
17+
18+
This accepts raw HTML, templates, JavaScript/Elixir/Lua string literals, or
19+
any other source-like text. It intentionally over-collects candidate-like
20+
tokens; use `validate/1` or `compile/2` to let the compiler decide which
21+
tokens are real utilities.
22+
23+
When passed a binary or iodata, this returns a list. When passed an enumerable
24+
such as `IO.stream/2` or `File.stream!/3`, this returns a lazy stream of
25+
unique candidate strings.
26+
27+
## Examples
28+
29+
TailwindCompiler.candidates(~s(<div class="flex p-4"></div>))
30+
#=> ["div", "flex", "p-4", "/div"]
31+
32+
File.stream!("index.html", [], :line)
33+
|> TailwindCompiler.candidates()
34+
|> Enum.to_list()
35+
36+
"""
37+
@spec candidates(iodata()) :: [String.t()]
38+
@spec candidates(Enumerable.t()) :: Enumerable.t()
39+
def candidates(source) when is_binary(source) or is_list(source) do
40+
TailwindCompiler.Candidates.extract(source)
41+
end
42+
43+
def candidates(source) do
44+
if Enumerable.impl_for(source) do
45+
TailwindCompiler.Candidates.stream(source)
46+
else
47+
raise ArgumentError, "expected source to be iodata or an enumerable of iodata chunks"
48+
end
49+
end
50+
51+
@doc """
52+
Extract candidates from source text and compile them into CSS.
53+
54+
This is a convenience wrapper around `candidates/1` and `compile/2`.
55+
"""
56+
@spec compile_source(iodata() | Enumerable.t(), keyword()) ::
57+
{:ok, String.t()} | {:error, term()}
58+
def compile_source(source, opts \\ []) do
59+
source
60+
|> candidates()
61+
|> compile(opts)
62+
end
63+
64+
@doc """
65+
Same as `compile_source/2` but raises on error.
66+
"""
67+
@spec compile_source!(iodata() | Enumerable.t(), keyword()) :: String.t()
68+
def compile_source!(source, opts \\ []) do
69+
case compile_source(source, opts) do
70+
{:ok, css} -> css
71+
{:error, reason} -> raise "TailwindCompiler.compile_source!/2 failed: #{inspect(reason)}"
72+
end
73+
end
74+
1575
@doc """
1676
Compile a list of Tailwind CSS candidate strings into minified CSS.
1777
@@ -50,8 +110,11 @@ defmodule TailwindCompiler do
50110
#=> {:ok, "...bg-primary{background-color:var(--color-primary)}..."}
51111
52112
"""
53-
@spec compile([String.t()], keyword()) :: {:ok, String.t()} | {:error, term()}
54-
def compile(candidates, opts \\ []) when is_list(candidates) do
113+
@spec compile([String.t()] | Enumerable.t(), keyword()) ::
114+
{:ok, String.t()} | {:error, term()}
115+
def compile(candidates, opts \\ [])
116+
117+
def compile(candidates, opts) when is_list(candidates) do
55118
theme_json = Keyword.get(opts, :theme)
56119
preflight = Keyword.get(opts, :preflight, true)
57120
minify = Keyword.get(opts, :minify, true)
@@ -66,14 +129,32 @@ defmodule TailwindCompiler do
66129
str when is_binary(str) -> str
67130
end
68131

69-
case TailwindCompiler.NIF.compile(candidates, theme_json || "", preflight, minify, custom_css || "", custom_utilities_json, plugin_css || "") do
132+
case TailwindCompiler.NIF.compile(
133+
candidates,
134+
theme_json || "",
135+
preflight,
136+
minify,
137+
custom_css || "",
138+
custom_utilities_json,
139+
plugin_css || ""
140+
) do
70141
result when is_binary(result) -> {:ok, result}
71142
error -> {:error, error}
72143
end
73144
rescue
74145
e -> {:error, Exception.message(e)}
75146
end
76147

148+
def compile(candidates, opts) do
149+
if Enumerable.impl_for(candidates) do
150+
candidates
151+
|> Enum.to_list()
152+
|> compile(opts)
153+
else
154+
{:error, "expected candidates to be a list or enumerable of strings"}
155+
end
156+
end
157+
77158
@doc """
78159
Same as `compile/2` but raises on error.
79160
"""

0 commit comments

Comments
 (0)